chore: Merge branch 'feat/expose' into script-setup-2
This commit is contained in:
commit
468e0d95cf
98
packages/runtime-core/__tests__/apiExpose.spec.ts
Normal file
98
packages/runtime-core/__tests__/apiExpose.spec.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { nodeOps, render } from '@vue/runtime-test'
|
||||||
|
import { defineComponent, h, ref } from '../src'
|
||||||
|
|
||||||
|
describe('api: expose', () => {
|
||||||
|
test('via setup context', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
render() {},
|
||||||
|
setup(_, { expose }) {
|
||||||
|
expose({
|
||||||
|
foo: ref(1),
|
||||||
|
bar: ref(2)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
bar: ref(3),
|
||||||
|
baz: ref(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const childRef = ref()
|
||||||
|
const Parent = {
|
||||||
|
setup() {
|
||||||
|
return () => h(Child, { ref: childRef })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Parent), root)
|
||||||
|
expect(childRef.value).toBeTruthy()
|
||||||
|
expect(childRef.value.foo).toBe(1)
|
||||||
|
expect(childRef.value.bar).toBe(2)
|
||||||
|
expect(childRef.value.baz).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('via options', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
render() {},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
foo: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
bar: ref(2),
|
||||||
|
baz: ref(3)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expose: ['foo', 'bar']
|
||||||
|
})
|
||||||
|
|
||||||
|
const childRef = ref()
|
||||||
|
const Parent = {
|
||||||
|
setup() {
|
||||||
|
return () => h(Child, { ref: childRef })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Parent), root)
|
||||||
|
expect(childRef.value).toBeTruthy()
|
||||||
|
expect(childRef.value.foo).toBe(1)
|
||||||
|
expect(childRef.value.bar).toBe(2)
|
||||||
|
expect(childRef.value.baz).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('options + context', () => {
|
||||||
|
const Child = defineComponent({
|
||||||
|
render() {},
|
||||||
|
expose: ['foo'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
foo: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(_, { expose }) {
|
||||||
|
expose({
|
||||||
|
bar: ref(2)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
bar: ref(3),
|
||||||
|
baz: ref(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const childRef = ref()
|
||||||
|
const Parent = {
|
||||||
|
setup() {
|
||||||
|
return () => h(Child, { ref: childRef })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const root = nodeOps.createElement('div')
|
||||||
|
render(h(Parent), root)
|
||||||
|
expect(childRef.value).toBeTruthy()
|
||||||
|
expect(childRef.value.foo).toBe(1)
|
||||||
|
expect(childRef.value.bar).toBe(2)
|
||||||
|
expect(childRef.value.baz).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
|
|||||||
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
|
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
|
||||||
extends ComponentInternalOptions {
|
extends ComponentInternalOptions {
|
||||||
// use of any here is intentional so it can be a valid JSX Element constructor
|
// use of any here is intentional so it can be a valid JSX Element constructor
|
||||||
(props: P, ctx: SetupContext<E, P>): any
|
(props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any
|
||||||
props?: ComponentPropsOptions<P>
|
props?: ComponentPropsOptions<P>
|
||||||
emits?: E | (keyof E)[]
|
emits?: E | (keyof E)[]
|
||||||
inheritAttrs?: boolean
|
inheritAttrs?: boolean
|
||||||
@ -172,6 +172,7 @@ export interface SetupContext<E = EmitsOptions, P = Data> {
|
|||||||
attrs: Data
|
attrs: Data
|
||||||
slots: Slots
|
slots: Slots
|
||||||
emit: EmitFn<E>
|
emit: EmitFn<E>
|
||||||
|
expose: (exposed: Record<string, any>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -271,6 +272,9 @@ export interface ComponentInternalInstance {
|
|||||||
// main proxy that serves as the public instance (`this`)
|
// main proxy that serves as the public instance (`this`)
|
||||||
proxy: ComponentPublicInstance | null
|
proxy: ComponentPublicInstance | null
|
||||||
|
|
||||||
|
// exposed properties via expose()
|
||||||
|
exposed: Record<string, any> | null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* alternative proxy used only for runtime-compiled render functions using
|
* alternative proxy used only for runtime-compiled render functions using
|
||||||
* `with` block
|
* `with` block
|
||||||
@ -416,6 +420,7 @@ export function createComponentInstance(
|
|||||||
update: null!, // will be set synchronously right after creation
|
update: null!, // will be set synchronously right after creation
|
||||||
render: null,
|
render: null,
|
||||||
proxy: null,
|
proxy: null,
|
||||||
|
exposed: null,
|
||||||
withProxy: null,
|
withProxy: null,
|
||||||
effects: null,
|
effects: null,
|
||||||
provides: parent ? parent.provides : Object.create(appContext.provides),
|
provides: parent ? parent.provides : Object.create(appContext.provides),
|
||||||
@ -732,6 +737,13 @@ const attrHandlers: ProxyHandler<Data> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
||||||
|
const expose: SetupContext['expose'] = exposed => {
|
||||||
|
if (__DEV__ && instance.exposed) {
|
||||||
|
warn(`expose() should be called only once per setup().`)
|
||||||
|
}
|
||||||
|
instance.exposed = proxyRefs(exposed)
|
||||||
|
}
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
// We use getters in dev in case libs like test-utils overwrite instance
|
// We use getters in dev in case libs like test-utils overwrite instance
|
||||||
// properties (overwrites should not be done in prod)
|
// properties (overwrites should not be done in prod)
|
||||||
@ -747,14 +759,16 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
|
|||||||
},
|
},
|
||||||
get emit() {
|
get emit() {
|
||||||
return (event: string, ...args: any[]) => instance.emit(event, ...args)
|
return (event: string, ...args: any[]) => instance.emit(event, ...args)
|
||||||
}
|
},
|
||||||
|
expose
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
props: instance.props,
|
props: instance.props,
|
||||||
attrs: instance.attrs,
|
attrs: instance.attrs,
|
||||||
slots: instance.slots,
|
slots: instance.slots,
|
||||||
emit: instance.emit
|
emit: instance.emit,
|
||||||
|
expose
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,9 @@ import {
|
|||||||
reactive,
|
reactive,
|
||||||
ComputedGetter,
|
ComputedGetter,
|
||||||
WritableComputedOptions,
|
WritableComputedOptions,
|
||||||
toRaw
|
toRaw,
|
||||||
|
proxyRefs,
|
||||||
|
toRef
|
||||||
} from '@vue/reactivity'
|
} from '@vue/reactivity'
|
||||||
import {
|
import {
|
||||||
ComponentObjectPropsOptions,
|
ComponentObjectPropsOptions,
|
||||||
@ -110,6 +112,8 @@ export interface ComponentOptionsBase<
|
|||||||
directives?: Record<string, Directive>
|
directives?: Record<string, Directive>
|
||||||
inheritAttrs?: boolean
|
inheritAttrs?: boolean
|
||||||
emits?: (E | EE[]) & ThisType<void>
|
emits?: (E | EE[]) & ThisType<void>
|
||||||
|
// TODO infer public instance type based on exposed keys
|
||||||
|
expose?: string[]
|
||||||
serverPrefetch?(): Promise<any>
|
serverPrefetch?(): Promise<any>
|
||||||
|
|
||||||
// Internal ------------------------------------------------------------------
|
// Internal ------------------------------------------------------------------
|
||||||
@ -461,7 +465,9 @@ export function applyOptions(
|
|||||||
render,
|
render,
|
||||||
renderTracked,
|
renderTracked,
|
||||||
renderTriggered,
|
renderTriggered,
|
||||||
errorCaptured
|
errorCaptured,
|
||||||
|
// public API
|
||||||
|
expose
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const publicThis = instance.proxy!
|
const publicThis = instance.proxy!
|
||||||
@ -736,6 +742,13 @@ export function applyOptions(
|
|||||||
if (unmounted) {
|
if (unmounted) {
|
||||||
onUnmounted(unmounted.bind(publicThis))
|
onUnmounted(unmounted.bind(publicThis))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asMixin && expose) {
|
||||||
|
const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
|
||||||
|
expose.forEach(key => {
|
||||||
|
exposed[key] = toRef(publicThis, key as any)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function callSyncHook(
|
function callSyncHook(
|
||||||
|
@ -306,12 +306,12 @@ export const setRef = (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let value: ComponentPublicInstance | RendererNode | null
|
let value: ComponentPublicInstance | RendererNode | Record<string, any> | null
|
||||||
if (!vnode) {
|
if (!vnode) {
|
||||||
value = null
|
value = null
|
||||||
} else {
|
} else {
|
||||||
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
|
||||||
value = vnode.component!.proxy
|
value = vnode.component!.exposed || vnode.component!.proxy
|
||||||
} else {
|
} else {
|
||||||
value = vnode.el
|
value = vnode.el
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ export type SchedulerCbs = SchedulerCb | SchedulerCb[]
|
|||||||
let isFlushing = false
|
let isFlushing = false
|
||||||
let isFlushPending = false
|
let isFlushPending = false
|
||||||
|
|
||||||
const queue: (SchedulerJob | null)[] = []
|
const queue: SchedulerJob[] = []
|
||||||
let flushIndex = 0
|
let flushIndex = 0
|
||||||
|
|
||||||
const pendingPreFlushCbs: SchedulerCb[] = []
|
const pendingPreFlushCbs: SchedulerCb[] = []
|
||||||
@ -87,7 +87,7 @@ function queueFlush() {
|
|||||||
export function invalidateJob(job: SchedulerJob) {
|
export function invalidateJob(job: SchedulerJob) {
|
||||||
const i = queue.indexOf(job)
|
const i = queue.indexOf(job)
|
||||||
if (i > -1) {
|
if (i > -1) {
|
||||||
queue[i] = null
|
queue.splice(i, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,9 +205,7 @@ function flushJobs(seen?: CountMap) {
|
|||||||
// priority number)
|
// priority number)
|
||||||
// 2. If a component is unmounted during a parent component's update,
|
// 2. If a component is unmounted during a parent component's update,
|
||||||
// its update can be skipped.
|
// its update can be skipped.
|
||||||
// Jobs can never be null before flush starts, since they are only invalidated
|
queue.sort((a, b) => getId(a) - getId(b))
|
||||||
// during execution of another flushed job.
|
|
||||||
queue.sort((a, b) => getId(a!) - getId(b!))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
|
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
|
||||||
|
@ -18,9 +18,9 @@ import {
|
|||||||
setTransitionHooks,
|
setTransitionHooks,
|
||||||
createVNode,
|
createVNode,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
SetupContext
|
SetupContext,
|
||||||
|
toRaw
|
||||||
} from '@vue/runtime-core'
|
} from '@vue/runtime-core'
|
||||||
import { toRaw } from '@vue/reactivity'
|
|
||||||
import { extend } from '@vue/shared'
|
import { extend } from '@vue/shared'
|
||||||
|
|
||||||
interface Position {
|
interface Position {
|
||||||
|
@ -8,10 +8,12 @@ export function initDev() {
|
|||||||
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__)
|
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__)
|
||||||
|
|
||||||
if (__BROWSER__) {
|
if (__BROWSER__) {
|
||||||
console.info(
|
if (!__ESM_BUNDLER__) {
|
||||||
`You are running a development build of Vue.\n` +
|
console.info(
|
||||||
`Make sure to use the production build (*.prod.js) when deploying for production.`
|
`You are running a development build of Vue.\n` +
|
||||||
)
|
`Make sure to use the production build (*.prod.js) when deploying for production.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
initCustomFormatter()
|
initCustomFormatter()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user