Skip to content

Commit 468e0d9

Browse files
committed
chore: Merge branch 'feat/expose' into script-setup-2
2 parents 1ff5960 + 0e59770 commit 468e0d9

File tree

8 files changed

+744
-587
lines changed

8 files changed

+744
-587
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { nodeOps, render } from '@vue/runtime-test'
2+
import { defineComponent, h, ref } from '../src'
3+
4+
describe('api: expose', () => {
5+
test('via setup context', () => {
6+
const Child = defineComponent({
7+
render() {},
8+
setup(_, { expose }) {
9+
expose({
10+
foo: ref(1),
11+
bar: ref(2)
12+
})
13+
return {
14+
bar: ref(3),
15+
baz: ref(4)
16+
}
17+
}
18+
})
19+
20+
const childRef = ref()
21+
const Parent = {
22+
setup() {
23+
return () => h(Child, { ref: childRef })
24+
}
25+
}
26+
const root = nodeOps.createElement('div')
27+
render(h(Parent), root)
28+
expect(childRef.value).toBeTruthy()
29+
expect(childRef.value.foo).toBe(1)
30+
expect(childRef.value.bar).toBe(2)
31+
expect(childRef.value.baz).toBeUndefined()
32+
})
33+
34+
test('via options', () => {
35+
const Child = defineComponent({
36+
render() {},
37+
data() {
38+
return {
39+
foo: 1
40+
}
41+
},
42+
setup() {
43+
return {
44+
bar: ref(2),
45+
baz: ref(3)
46+
}
47+
},
48+
expose: ['foo', 'bar']
49+
})
50+
51+
const childRef = ref()
52+
const Parent = {
53+
setup() {
54+
return () => h(Child, { ref: childRef })
55+
}
56+
}
57+
const root = nodeOps.createElement('div')
58+
render(h(Parent), root)
59+
expect(childRef.value).toBeTruthy()
60+
expect(childRef.value.foo).toBe(1)
61+
expect(childRef.value.bar).toBe(2)
62+
expect(childRef.value.baz).toBeUndefined()
63+
})
64+
65+
test('options + context', () => {
66+
const Child = defineComponent({
67+
render() {},
68+
expose: ['foo'],
69+
data() {
70+
return {
71+
foo: 1
72+
}
73+
},
74+
setup(_, { expose }) {
75+
expose({
76+
bar: ref(2)
77+
})
78+
return {
79+
bar: ref(3),
80+
baz: ref(4)
81+
}
82+
}
83+
})
84+
85+
const childRef = ref()
86+
const Parent = {
87+
setup() {
88+
return () => h(Child, { ref: childRef })
89+
}
90+
}
91+
const root = nodeOps.createElement('div')
92+
render(h(Parent), root)
93+
expect(childRef.value).toBeTruthy()
94+
expect(childRef.value.foo).toBe(1)
95+
expect(childRef.value.bar).toBe(2)
96+
expect(childRef.value.baz).toBeUndefined()
97+
})
98+
})

packages/runtime-core/src/component.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface ComponentInternalOptions {
105105
export interface FunctionalComponent<P = {}, E extends EmitsOptions = {}>
106106
extends ComponentInternalOptions {
107107
// use of any here is intentional so it can be a valid JSX Element constructor
108-
(props: P, ctx: SetupContext<E, P>): any
108+
(props: P, ctx: Omit<SetupContext<E, P>, 'expose'>): any
109109
props?: ComponentPropsOptions<P>
110110
emits?: E | (keyof E)[]
111111
inheritAttrs?: boolean
@@ -172,6 +172,7 @@ export interface SetupContext<E = EmitsOptions, P = Data> {
172172
attrs: Data
173173
slots: Slots
174174
emit: EmitFn<E>
175+
expose: (exposed: Record<string, any>) => void
175176
}
176177

177178
/**
@@ -271,6 +272,9 @@ export interface ComponentInternalInstance {
271272
// main proxy that serves as the public instance (`this`)
272273
proxy: ComponentPublicInstance | null
273274

275+
// exposed properties via expose()
276+
exposed: Record<string, any> | null
277+
274278
/**
275279
* alternative proxy used only for runtime-compiled render functions using
276280
* `with` block
@@ -416,6 +420,7 @@ export function createComponentInstance(
416420
update: null!, // will be set synchronously right after creation
417421
render: null,
418422
proxy: null,
423+
exposed: null,
419424
withProxy: null,
420425
effects: null,
421426
provides: parent ? parent.provides : Object.create(appContext.provides),
@@ -732,6 +737,13 @@ const attrHandlers: ProxyHandler<Data> = {
732737
}
733738

734739
function createSetupContext(instance: ComponentInternalInstance): SetupContext {
740+
const expose: SetupContext['expose'] = exposed => {
741+
if (__DEV__ && instance.exposed) {
742+
warn(`expose() should be called only once per setup().`)
743+
}
744+
instance.exposed = proxyRefs(exposed)
745+
}
746+
735747
if (__DEV__) {
736748
// We use getters in dev in case libs like test-utils overwrite instance
737749
// properties (overwrites should not be done in prod)
@@ -747,14 +759,16 @@ function createSetupContext(instance: ComponentInternalInstance): SetupContext {
747759
},
748760
get emit() {
749761
return (event: string, ...args: any[]) => instance.emit(event, ...args)
750-
}
762+
},
763+
expose
751764
})
752765
} else {
753766
return {
754767
props: instance.props,
755768
attrs: instance.attrs,
756769
slots: instance.slots,
757-
emit: instance.emit
770+
emit: instance.emit,
771+
expose
758772
}
759773
}
760774
}

packages/runtime-core/src/componentOptions.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ import {
4141
reactive,
4242
ComputedGetter,
4343
WritableComputedOptions,
44-
toRaw
44+
toRaw,
45+
proxyRefs,
46+
toRef
4547
} from '@vue/reactivity'
4648
import {
4749
ComponentObjectPropsOptions,
@@ -110,6 +112,8 @@ export interface ComponentOptionsBase<
110112
directives?: Record<string, Directive>
111113
inheritAttrs?: boolean
112114
emits?: (E | EE[]) & ThisType<void>
115+
// TODO infer public instance type based on exposed keys
116+
expose?: string[]
113117
serverPrefetch?(): Promise<any>
114118

115119
// Internal ------------------------------------------------------------------
@@ -461,7 +465,9 @@ export function applyOptions(
461465
render,
462466
renderTracked,
463467
renderTriggered,
464-
errorCaptured
468+
errorCaptured,
469+
// public API
470+
expose
465471
} = options
466472

467473
const publicThis = instance.proxy!
@@ -736,6 +742,13 @@ export function applyOptions(
736742
if (unmounted) {
737743
onUnmounted(unmounted.bind(publicThis))
738744
}
745+
746+
if (!asMixin && expose) {
747+
const exposed = instance.exposed || (instance.exposed = proxyRefs({}))
748+
expose.forEach(key => {
749+
exposed[key] = toRef(publicThis, key as any)
750+
})
751+
}
739752
}
740753

741754
function callSyncHook(

packages/runtime-core/src/renderer.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,12 +306,12 @@ export const setRef = (
306306
return
307307
}
308308

309-
let value: ComponentPublicInstance | RendererNode | null
309+
let value: ComponentPublicInstance | RendererNode | Record<string, any> | null
310310
if (!vnode) {
311311
value = null
312312
} else {
313313
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
314-
value = vnode.component!.proxy
314+
value = vnode.component!.exposed || vnode.component!.proxy
315315
} else {
316316
value = vnode.el
317317
}

packages/runtime-core/src/scheduler.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export type SchedulerCbs = SchedulerCb | SchedulerCb[]
3030
let isFlushing = false
3131
let isFlushPending = false
3232

33-
const queue: (SchedulerJob | null)[] = []
33+
const queue: SchedulerJob[] = []
3434
let flushIndex = 0
3535

3636
const pendingPreFlushCbs: SchedulerCb[] = []
@@ -87,7 +87,7 @@ function queueFlush() {
8787
export function invalidateJob(job: SchedulerJob) {
8888
const i = queue.indexOf(job)
8989
if (i > -1) {
90-
queue[i] = null
90+
queue.splice(i, 1)
9191
}
9292
}
9393

@@ -205,9 +205,7 @@ function flushJobs(seen?: CountMap) {
205205
// priority number)
206206
// 2. If a component is unmounted during a parent component's update,
207207
// its update can be skipped.
208-
// Jobs can never be null before flush starts, since they are only invalidated
209-
// during execution of another flushed job.
210-
queue.sort((a, b) => getId(a!) - getId(b!))
208+
queue.sort((a, b) => getId(a) - getId(b))
211209

212210
try {
213211
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {

packages/runtime-dom/src/components/TransitionGroup.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ import {
1818
setTransitionHooks,
1919
createVNode,
2020
onUpdated,
21-
SetupContext
21+
SetupContext,
22+
toRaw
2223
} from '@vue/runtime-core'
23-
import { toRaw } from '@vue/reactivity'
2424
import { extend } from '@vue/shared'
2525

2626
interface Position {

packages/vue/src/dev.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ export function initDev() {
88
setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__)
99

1010
if (__BROWSER__) {
11-
console.info(
12-
`You are running a development build of Vue.\n` +
13-
`Make sure to use the production build (*.prod.js) when deploying for production.`
14-
)
11+
if (!__ESM_BUNDLER__) {
12+
console.info(
13+
`You are running a development build of Vue.\n` +
14+
`Make sure to use the production build (*.prod.js) when deploying for production.`
15+
)
16+
}
1517

1618
initCustomFormatter()
1719
}

0 commit comments

Comments
 (0)