Skip to content

Commit 24e9efc

Browse files
committed
refactor(runtime-core): extract component emit related logic into dedicated file
1 parent bf473a6 commit 24e9efc

File tree

10 files changed

+128
-103
lines changed

10 files changed

+128
-103
lines changed

packages/runtime-core/src/apiCreateApp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
validateComponentName,
55
PublicAPIComponent
66
} from './component'
7-
import { ComponentOptions } from './apiOptions'
7+
import { ComponentOptions } from './componentOptions'
88
import { ComponentPublicInstance } from './componentProxy'
99
import { Directive, validateDirectiveName } from './directives'
1010
import { RootRenderFunction } from './renderer'

packages/runtime-core/src/apiDefineComponent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import {
33
MethodOptions,
44
ComponentOptionsWithoutProps,
55
ComponentOptionsWithArrayProps,
6-
ComponentOptionsWithObjectProps,
7-
EmitsOptions
8-
} from './apiOptions'
6+
ComponentOptionsWithObjectProps
7+
} from './componentOptions'
98
import { SetupContext, RenderFunction } from './component'
109
import { ComponentPublicInstance } from './componentProxy'
1110
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
11+
import { EmitsOptions } from './componentEmits'
1212
import { isFunction } from '@vue/shared'
1313
import { VNodeProps } from './vnode'
1414

packages/runtime-core/src/component.ts

Lines changed: 13 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,24 @@ import {
1414
import { ComponentPropsOptions, resolveProps } from './componentProps'
1515
import { Slots, resolveSlots } from './componentSlots'
1616
import { warn } from './warning'
17-
import {
18-
ErrorCodes,
19-
callWithErrorHandling,
20-
callWithAsyncErrorHandling
21-
} from './errorHandling'
17+
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
2218
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
2319
import { Directive, validateDirectiveName } from './directives'
24-
import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
20+
import { applyOptions, ComponentOptions } from './componentOptions'
21+
import {
22+
EmitsOptions,
23+
ObjectEmitsOptions,
24+
EmitFn,
25+
emit
26+
} from './componentEmits'
2527
import {
2628
EMPTY_OBJ,
2729
isFunction,
28-
capitalize,
2930
NOOP,
3031
isObject,
3132
NO,
3233
makeMap,
3334
isPromise,
34-
isArray,
35-
hyphenate,
3635
ShapeFlags
3736
} from '@vue/shared'
3837
import { SuspenseBoundary } from './components/Suspense'
@@ -96,29 +95,10 @@ export const enum LifecycleHooks {
9695
ERROR_CAPTURED = 'ec'
9796
}
9897

99-
type UnionToIntersection<U> = (U extends any
100-
? (k: U) => void
101-
: never) extends ((k: infer I) => void)
102-
? I
103-
: never
104-
105-
export type Emit<
106-
Options = Record<string, any>,
107-
Event extends keyof Options = keyof Options
108-
> = Options extends any[]
109-
? (event: Options[0], ...args: any[]) => unknown[]
110-
: UnionToIntersection<
111-
{
112-
[key in Event]: Options[key] extends ((...args: infer Args) => any)
113-
? (event: key, ...args: Args) => unknown[]
114-
: (event: key, ...args: any[]) => unknown[]
115-
}[Event]
116-
>
117-
118-
export interface SetupContext<E = Record<string, any>> {
98+
export interface SetupContext<E = ObjectEmitsOptions> {
11999
attrs: Data
120100
slots: Slots
121-
emit: Emit<E>
101+
emit: EmitFn<E>
122102
}
123103

124104
export type RenderFunction = {
@@ -165,7 +145,7 @@ export interface ComponentInternalInstance {
165145
propsProxy: Data | null
166146
setupContext: SetupContext | null
167147
refs: Data
168-
emit: Emit
148+
emit: EmitFn
169149

170150
// suspense related
171151
suspense: SuspenseBoundary | null
@@ -268,29 +248,10 @@ export function createComponentInstance(
268248
rtg: null,
269249
rtc: null,
270250
ec: null,
271-
272-
emit: (event: string, ...args: any[]): any[] => {
273-
const props = instance.vnode.props || EMPTY_OBJ
274-
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
275-
if (!handler && event.indexOf('update:') === 0) {
276-
event = hyphenate(event)
277-
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
278-
}
279-
if (handler) {
280-
const res = callWithAsyncErrorHandling(
281-
handler,
282-
instance,
283-
ErrorCodes.COMPONENT_EVENT_HANDLER,
284-
args
285-
)
286-
return isArray(res) ? res : [res]
287-
} else {
288-
return []
289-
}
290-
}
251+
emit: null as any // to be set immediately
291252
}
292-
293253
instance.root = parent ? parent.root : instance
254+
instance.emit = emit.bind(null, instance)
294255
return instance
295256
}
296257

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
isArray,
3+
isOn,
4+
hasOwn,
5+
EMPTY_OBJ,
6+
capitalize,
7+
hyphenate
8+
} from '@vue/shared'
9+
import { ComponentInternalInstance } from './component'
10+
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
11+
12+
export type ObjectEmitsOptions = Record<
13+
string,
14+
((...args: any[]) => any) | null
15+
>
16+
export type EmitsOptions = ObjectEmitsOptions | string[]
17+
18+
type UnionToIntersection<U> = (U extends any
19+
? (k: U) => void
20+
: never) extends ((k: infer I) => void)
21+
? I
22+
: never
23+
24+
export type EmitFn<
25+
Options = ObjectEmitsOptions,
26+
Event extends keyof Options = keyof Options
27+
> = Options extends any[]
28+
? (event: Options[0], ...args: any[]) => unknown[]
29+
: UnionToIntersection<
30+
{
31+
[key in Event]: Options[key] extends ((...args: infer Args) => any)
32+
? (event: key, ...args: Args) => unknown[]
33+
: (event: key, ...args: any[]) => unknown[]
34+
}[Event]
35+
>
36+
37+
export function emit(
38+
instance: ComponentInternalInstance,
39+
event: string,
40+
...args: any[]
41+
): any[] {
42+
const props = instance.vnode.props || EMPTY_OBJ
43+
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
44+
// for v-model update:xxx events, also trigger kebab-case equivalent
45+
// for props passed via kebab-case
46+
if (!handler && event.indexOf('update:') === 0) {
47+
event = hyphenate(event)
48+
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
49+
}
50+
if (handler) {
51+
const res = callWithAsyncErrorHandling(
52+
handler,
53+
instance,
54+
ErrorCodes.COMPONENT_EVENT_HANDLER,
55+
args
56+
)
57+
return isArray(res) ? res : [res]
58+
} else {
59+
return []
60+
}
61+
}
62+
63+
export function normalizeEmitsOptions(
64+
options: EmitsOptions | undefined
65+
): ObjectEmitsOptions | undefined {
66+
if (!options) {
67+
return
68+
} else if (isArray(options)) {
69+
if ((options as any)._n) {
70+
return (options as any)._n
71+
}
72+
const normalized: ObjectEmitsOptions = {}
73+
options.forEach(key => (normalized[key] = null))
74+
Object.defineProperty(options, '_n', { value: normalized })
75+
return normalized
76+
} else {
77+
return options
78+
}
79+
}
80+
81+
// Check if an incoming prop key is a declared emit event listener.
82+
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
83+
// both considered matched listeners.
84+
export function isEmitListener(
85+
emits: ObjectEmitsOptions,
86+
key: string
87+
): boolean {
88+
return (
89+
isOn(key) &&
90+
(hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
91+
hasOwn(emits, key.slice(2)))
92+
)
93+
}

packages/runtime-core/src/apiOptions.ts renamed to packages/runtime-core/src/componentOptions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
WritableComputedOptions
4242
} from '@vue/reactivity'
4343
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
44+
import { EmitsOptions } from './componentEmits'
4445
import { Directive } from './directives'
4546
import { ComponentPublicInstance } from './componentProxy'
4647
import { warn } from './warning'
@@ -149,8 +150,6 @@ export interface MethodOptions {
149150
[key: string]: Function
150151
}
151152

152-
export type EmitsOptions = Record<string, any> | string[]
153-
154153
export type ExtractComputedReturns<T extends any> = {
155154
[key in keyof T]: T[key] extends { get: Function }
156155
? ReturnType<T[key]['get']>

packages/runtime-core/src/componentProps.ts

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import {
1414
makeMap,
1515
isReservedProp,
1616
EMPTY_ARR,
17-
ShapeFlags,
18-
isOn
17+
ShapeFlags
1918
} from '@vue/shared'
2019
import { warn } from './warning'
2120
import { Data, ComponentInternalInstance } from './component'
22-
import { EmitsOptions } from './apiOptions'
21+
import { normalizeEmitsOptions, isEmitListener } from './componentEmits'
2322

2423
export type ComponentPropsOptions<P = Data> =
2524
| ComponentObjectPropsOptions<P>
@@ -147,7 +146,7 @@ export function resolveProps(
147146
let camelKey
148147
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
149148
setProp(camelKey, value)
150-
} else if (!emits || !isListener(emits, key)) {
149+
} else if (!emits || !isEmitListener(emits, key)) {
151150
// Any non-declared (either as a prop or an emitted event) props are put
152151
// into a separate `attrs` object for spreading. Make sure to preserve
153152
// original key casing
@@ -281,35 +280,6 @@ export function normalizePropsOptions(
281280
return normalizedEntry
282281
}
283282

284-
function normalizeEmitsOptions(
285-
options: EmitsOptions | undefined
286-
): Record<string, any> | undefined {
287-
if (!options) {
288-
return
289-
} else if (isArray(options)) {
290-
if ((options as any)._n) {
291-
return (options as any)._n
292-
}
293-
const normalized: Record<string, null> = {}
294-
options.forEach(key => (normalized[key] = null))
295-
Object.defineProperty(options, '_n', normalized)
296-
return normalized
297-
} else {
298-
return options
299-
}
300-
}
301-
302-
function isListener(emits: Record<string, any>, key: string): boolean {
303-
if (!isOn(key)) {
304-
return false
305-
}
306-
const eventName = key.slice(2)
307-
return (
308-
hasOwn(emits, eventName) ||
309-
hasOwn(emits, eventName[0].toLowerCase() + eventName.slice(1))
310-
)
311-
}
312-
313283
// use function string name to check type constructors
314284
// so that it works across vms / iframes.
315285
function getType(ctor: Prop<any>): string {

packages/runtime-core/src/componentProxy.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { ComponentInternalInstance, Data, Emit } from './component'
1+
import { ComponentInternalInstance, Data } from './component'
22
import { nextTick, queueJob } from './scheduler'
33
import { instanceWatch } from './apiWatch'
44
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
5+
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
56
import {
67
ExtractComputedReturns,
78
ComponentOptionsBase,
89
ComputedOptions,
910
MethodOptions,
10-
resolveMergedOptions,
11-
EmitsOptions
12-
} from './apiOptions'
13-
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
14-
import { warn } from './warning'
11+
resolveMergedOptions
12+
} from './componentOptions'
13+
import { normalizePropsOptions } from './componentProps'
14+
import { EmitsOptions, EmitFn } from './componentEmits'
1515
import { Slots } from './componentSlots'
1616
import {
1717
currentRenderingInstance,
1818
markAttrsAccessed
1919
} from './componentRenderUtils'
20-
import { normalizePropsOptions } from './componentProps'
20+
import { warn } from './warning'
2121

2222
// public properties exposed on the proxy, which is used as the render context
2323
// in templates (as `this` in the render option)
@@ -38,7 +38,7 @@ export type ComponentPublicInstance<
3838
$slots: Slots
3939
$root: ComponentInternalInstance | null
4040
$parent: ComponentInternalInstance | null
41-
$emit: Emit<E>
41+
$emit: EmitFn<E>
4242
$el: any
4343
$options: ComponentOptionsBase<P, B, D, C, M, E>
4444
$forceUpdate: ReactiveEffect

packages/runtime-core/src/h.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
ComponentOptionsWithArrayProps,
1717
ComponentOptionsWithObjectProps,
1818
ComponentOptions
19-
} from './apiOptions'
19+
} from './componentOptions'
2020
import { ExtractPropTypes } from './componentProps'
2121

2222
// `h` is a more user-friendly version of `createVNode` that allows omitting the

packages/runtime-core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export {
186186
ComponentOptionsWithoutProps,
187187
ComponentOptionsWithObjectProps as ComponentOptionsWithProps,
188188
ComponentOptionsWithArrayProps
189-
} from './apiOptions'
189+
} from './componentOptions'
190190
export { ComponentPublicInstance } from './componentProxy'
191191
export {
192192
Renderer,

test-dts/functionalComponent.test-d.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ const Foo = (props: { foo: number }) => props.foo
66

77
// TSX
88
expectType<JSX.Element>(<Foo foo={1} />)
9-
expectError(<Foo />)
9+
// expectError(<Foo />) // tsd does not catch missing type errors
1010
expectError(<Foo foo="bar" />)
11+
expectError(<Foo baz="bar" />)
1112

1213
// Explicit signature with props + emits
1314
const Bar: FunctionalComponent<
@@ -35,5 +36,6 @@ expectError((Bar.emits = { baz: () => void 0 }))
3536

3637
// TSX
3738
expectType<JSX.Element>(<Bar foo={1} />)
38-
expectError(<Bar />)
39+
// expectError(<Foo />) // tsd does not catch missing type errors
3940
expectError(<Bar foo="bar" />)
41+
expectError(<Foo baz="bar" />)

0 commit comments

Comments
 (0)