Skip to content

Commit 8ed0b34

Browse files
committed
fix(runtime-core): fix props/emits resolving with global mixins
fix #1975
1 parent 2bbeea9 commit 8ed0b34

8 files changed

+162
-101
lines changed

packages/runtime-core/__tests__/componentEmits.spec.ts

+8-35
Original file line numberDiff line numberDiff line change
@@ -178,40 +178,13 @@ describe('component: emit', () => {
178178
expect(fn).toHaveBeenCalledTimes(1)
179179
})
180180

181-
describe('isEmitListener', () => {
182-
test('array option', () => {
183-
const def1 = { emits: ['click'] }
184-
expect(isEmitListener(def1, 'onClick')).toBe(true)
185-
expect(isEmitListener(def1, 'onclick')).toBe(false)
186-
expect(isEmitListener(def1, 'onBlick')).toBe(false)
187-
})
188-
189-
test('object option', () => {
190-
const def2 = { emits: { click: null } }
191-
expect(isEmitListener(def2, 'onClick')).toBe(true)
192-
expect(isEmitListener(def2, 'onclick')).toBe(false)
193-
expect(isEmitListener(def2, 'onBlick')).toBe(false)
194-
})
195-
196-
test('with mixins and extends', () => {
197-
const mixin1 = { emits: ['foo'] }
198-
const mixin2 = { emits: ['bar'] }
199-
const extend = { emits: ['baz'] }
200-
const def3 = {
201-
mixins: [mixin1, mixin2],
202-
extends: extend
203-
}
204-
expect(isEmitListener(def3, 'onFoo')).toBe(true)
205-
expect(isEmitListener(def3, 'onBar')).toBe(true)
206-
expect(isEmitListener(def3, 'onBaz')).toBe(true)
207-
expect(isEmitListener(def3, 'onclick')).toBe(false)
208-
expect(isEmitListener(def3, 'onBlick')).toBe(false)
209-
})
210-
211-
test('.once listeners', () => {
212-
const def2 = { emits: { click: null } }
213-
expect(isEmitListener(def2, 'onClickOnce')).toBe(true)
214-
expect(isEmitListener(def2, 'onclickOnce')).toBe(false)
215-
})
181+
test('isEmitListener', () => {
182+
const options = { click: null }
183+
expect(isEmitListener(options, 'onClick')).toBe(true)
184+
expect(isEmitListener(options, 'onclick')).toBe(false)
185+
expect(isEmitListener(options, 'onBlick')).toBe(false)
186+
// .once listeners
187+
expect(isEmitListener(options, 'onClickOnce')).toBe(true)
188+
expect(isEmitListener(options, 'onclickOnce')).toBe(false)
216189
})
217190
})

packages/runtime-core/__tests__/componentProps.spec.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
FunctionalComponent,
88
defineComponent,
99
ref,
10-
serializeInner
10+
serializeInner,
11+
createApp
1112
} from '@vue/runtime-test'
1213
import { render as domRender, nextTick } from 'vue'
1314

@@ -309,4 +310,44 @@ describe('component props', () => {
309310
expect(setupProps).toMatchObject(props)
310311
expect(renderProxy.$props).toMatchObject(props)
311312
})
313+
314+
test('merging props from global mixins', () => {
315+
let setupProps: any
316+
let renderProxy: any
317+
318+
const M1 = {
319+
props: ['m1']
320+
}
321+
const M2 = {
322+
props: { m2: null }
323+
}
324+
const Comp = {
325+
props: ['self'],
326+
setup(props: any) {
327+
setupProps = props
328+
},
329+
render(this: any) {
330+
renderProxy = this
331+
return h('div', [this.self, this.m1, this.m2])
332+
}
333+
}
334+
335+
const props = {
336+
self: 'from self, ',
337+
m1: 'from mixin 1, ',
338+
m2: 'from mixin 2'
339+
}
340+
const app = createApp(Comp, props)
341+
app.mixin(M1)
342+
app.mixin(M2)
343+
344+
const root = nodeOps.createElement('div')
345+
app.mount(root)
346+
347+
expect(serializeInner(root)).toMatch(
348+
`from self, from mixin 1, from mixin 2`
349+
)
350+
expect(setupProps).toMatchObject(props)
351+
expect(renderProxy.$props).toMatchObject(props)
352+
})
312353
})

packages/runtime-core/src/apiCreateApp.ts

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface App<HostElement = any> {
3333
provide<T>(key: InjectionKey<T> | string, value: T): this
3434

3535
// internal, but we need to expose these for the server-renderer and devtools
36+
_uid: number
3637
_component: ConcreteComponent
3738
_props: Data | null
3839
_container: HostElement | null
@@ -108,6 +109,8 @@ export type CreateAppFunction<HostElement> = (
108109
rootProps?: Data | null
109110
) => App<HostElement>
110111

112+
let uid = 0
113+
111114
export function createAppAPI<HostElement>(
112115
render: RootRenderFunction,
113116
hydrate?: RootHydrateFunction
@@ -124,6 +127,7 @@ export function createAppAPI<HostElement>(
124127
let isMounted = false
125128

126129
const app: App = (context.app = {
130+
_uid: uid++,
127131
_component: rootComponent as ConcreteComponent,
128132
_props: rootProps,
129133
_container: null,

packages/runtime-core/src/component.ts

+30-9
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import {
1818
import {
1919
ComponentPropsOptions,
2020
NormalizedPropsOptions,
21-
initProps
21+
initProps,
22+
normalizePropsOptions
2223
} from './componentProps'
2324
import { Slots, initSlots, InternalSlots } from './componentSlots'
2425
import { warn } from './warning'
@@ -30,7 +31,8 @@ import {
3031
EmitsOptions,
3132
ObjectEmitsOptions,
3233
EmitFn,
33-
emit
34+
emit,
35+
normalizeEmitsOptions
3436
} from './componentEmits'
3537
import {
3638
EMPTY_OBJ,
@@ -72,11 +74,11 @@ export interface ComponentInternalOptions {
7274
/**
7375
* @internal
7476
*/
75-
__props?: NormalizedPropsOptions | []
77+
__props?: Record<number, NormalizedPropsOptions>
7678
/**
7779
* @internal
7880
*/
79-
__emits?: ObjectEmitsOptions
81+
__emits?: Record<number, ObjectEmitsOptions | null>
8082
/**
8183
* @internal
8284
*/
@@ -231,6 +233,16 @@ export interface ComponentInternalInstance {
231233
* @internal
232234
*/
233235
directives: Record<string, Directive> | null
236+
/**
237+
* reoslved props options
238+
* @internal
239+
*/
240+
propsOptions: NormalizedPropsOptions
241+
/**
242+
* resolved emits options
243+
* @internal
244+
*/
245+
emitsOptions: ObjectEmitsOptions | null
234246

235247
// the rest are only for stateful components ---------------------------------
236248

@@ -254,14 +266,17 @@ export interface ComponentInternalInstance {
254266
*/
255267
ctx: Data
256268

257-
// internal state
269+
// state
258270
data: Data
259271
props: Data
260272
attrs: Data
261273
slots: InternalSlots
262274
refs: Data
263275
emit: EmitFn
264-
// used for keeping track of .once event handlers on components
276+
/**
277+
* used for keeping track of .once event handlers on components
278+
* @internal
279+
*/
265280
emitted: Record<string, boolean> | null
266281

267282
/**
@@ -387,6 +402,14 @@ export function createComponentInstance(
387402
components: null,
388403
directives: null,
389404

405+
// resolved props and emits options
406+
propsOptions: normalizePropsOptions(type, appContext),
407+
emitsOptions: normalizeEmitsOptions(type, appContext),
408+
409+
// emit
410+
emit: null as any, // to be set immediately
411+
emitted: null,
412+
390413
// state
391414
ctx: EMPTY_OBJ,
392415
data: EMPTY_OBJ,
@@ -419,9 +442,7 @@ export function createComponentInstance(
419442
a: null,
420443
rtg: null,
421444
rtc: null,
422-
ec: null,
423-
emit: null as any, // to be set immediately
424-
emitted: null
445+
ec: null
425446
}
426447
if (__DEV__) {
427448
instance.ctx = createRenderContext(instance)

packages/runtime-core/src/componentEmits.ts

+41-23
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import {
88
isFunction,
99
extend
1010
} from '@vue/shared'
11-
import { ComponentInternalInstance, ConcreteComponent } from './component'
11+
import {
12+
ComponentInternalInstance,
13+
ComponentOptions,
14+
ConcreteComponent
15+
} from './component'
1216
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
1317
import { warn } from './warning'
14-
import { normalizePropsOptions } from './componentProps'
1518
import { UnionToIntersection } from './helpers/typeUtils'
1619
import { devtoolsComponentEmit } from './devtools'
20+
import { AppContext } from './apiCreateApp'
1721

1822
export type ObjectEmitsOptions = Record<
1923
string,
@@ -44,18 +48,20 @@ export function emit(
4448
const props = instance.vnode.props || EMPTY_OBJ
4549

4650
if (__DEV__) {
47-
const options = normalizeEmitsOptions(instance.type)
48-
if (options) {
49-
if (!(event in options)) {
50-
const propsOptions = normalizePropsOptions(instance.type)[0]
51+
const {
52+
emitsOptions,
53+
propsOptions: [propsOptions]
54+
} = instance
55+
if (emitsOptions) {
56+
if (!(event in emitsOptions)) {
5157
if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) {
5258
warn(
5359
`Component emitted event "${event}" but it is neither declared in ` +
5460
`the emits option nor as an "on${capitalize(event)}" prop.`
5561
)
5662
}
5763
} else {
58-
const validator = options[event]
64+
const validator = emitsOptions[event]
5965
if (isFunction(validator)) {
6066
const isValid = validator(...args)
6167
if (!isValid) {
@@ -98,11 +104,16 @@ export function emit(
98104
}
99105
}
100106

101-
function normalizeEmitsOptions(
102-
comp: ConcreteComponent
103-
): ObjectEmitsOptions | undefined {
104-
if (hasOwn(comp, '__emits')) {
105-
return comp.__emits
107+
export function normalizeEmitsOptions(
108+
comp: ConcreteComponent,
109+
appContext: AppContext,
110+
asMixin = false
111+
): ObjectEmitsOptions | null {
112+
const appId = appContext.app ? appContext.app._uid : -1
113+
const cache = comp.__emits || (comp.__emits = {})
114+
const cached = cache[appId]
115+
if (cached !== undefined) {
116+
return cached
106117
}
107118

108119
const raw = comp.emits
@@ -111,39 +122,46 @@ function normalizeEmitsOptions(
111122
// apply mixin/extends props
112123
let hasExtends = false
113124
if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
114-
if (comp.extends) {
125+
const extendEmits = (raw: ComponentOptions) => {
115126
hasExtends = true
116-
extend(normalized, normalizeEmitsOptions(comp.extends))
127+
extend(normalized, normalizeEmitsOptions(raw, appContext, true))
128+
}
129+
if (!asMixin && appContext.mixins.length) {
130+
appContext.mixins.forEach(extendEmits)
131+
}
132+
if (comp.extends) {
133+
extendEmits(comp.extends)
117134
}
118135
if (comp.mixins) {
119-
hasExtends = true
120-
comp.mixins.forEach(m => extend(normalized, normalizeEmitsOptions(m)))
136+
comp.mixins.forEach(extendEmits)
121137
}
122138
}
123139

124140
if (!raw && !hasExtends) {
125-
return (comp.__emits = undefined)
141+
return (cache[appId] = null)
126142
}
127143

128144
if (isArray(raw)) {
129145
raw.forEach(key => (normalized[key] = null))
130146
} else {
131147
extend(normalized, raw)
132148
}
133-
return (comp.__emits = normalized)
149+
return (cache[appId] = normalized)
134150
}
135151

136152
// Check if an incoming prop key is a declared emit event listener.
137153
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
138154
// both considered matched listeners.
139-
export function isEmitListener(comp: ConcreteComponent, key: string): boolean {
140-
let emits: ObjectEmitsOptions | undefined
141-
if (!isOn(key) || !(emits = normalizeEmitsOptions(comp))) {
155+
export function isEmitListener(
156+
options: ObjectEmitsOptions | null,
157+
key: string
158+
): boolean {
159+
if (!options || !isOn(key)) {
142160
return false
143161
}
144162
key = key.replace(/Once$/, '')
145163
return (
146-
hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
147-
hasOwn(emits, key.slice(2))
164+
hasOwn(options, key[2].toLowerCase() + key.slice(3)) ||
165+
hasOwn(options, key.slice(2))
148166
)
149167
}

packages/runtime-core/src/componentOptions.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@ import {
4242
WritableComputedOptions,
4343
toRaw
4444
} from '@vue/reactivity'
45-
import {
46-
ComponentObjectPropsOptions,
47-
ExtractPropTypes,
48-
normalizePropsOptions
49-
} from './componentProps'
45+
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
5046
import { EmitsOptions } from './componentEmits'
5147
import { Directive } from './directives'
5248
import {
@@ -431,7 +427,7 @@ export function applyOptions(
431427
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
432428

433429
if (__DEV__) {
434-
const propsOptions = normalizePropsOptions(options)[0]
430+
const [propsOptions] = instance.propsOptions
435431
if (propsOptions) {
436432
for (const key in propsOptions) {
437433
checkDuplicateProperties!(OptionTypes.PROPS, key)

0 commit comments

Comments
 (0)