chore: Merge branch 'feat/expose' into script-setup-2

This commit is contained in:
Evan You 2020-11-14 12:50:32 -05:00
commit 468e0d95cf
8 changed files with 741 additions and 584 deletions

View 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()
})
})

View File

@ -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
} }
} }
} }

View File

@ -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(

View File

@ -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
} }

View File

@ -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++) {

View File

@ -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 {

View File

@ -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()
} }

1164
yarn.lock

File diff suppressed because it is too large Load Diff