refactor(runtime-core): extract component emit related logic into dedicated file
This commit is contained in:
parent
bf473a64ea
commit
24e9efcc21
@ -4,7 +4,7 @@ import {
|
||||
validateComponentName,
|
||||
PublicAPIComponent
|
||||
} from './component'
|
||||
import { ComponentOptions } from './apiOptions'
|
||||
import { ComponentOptions } from './componentOptions'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { Directive, validateDirectiveName } from './directives'
|
||||
import { RootRenderFunction } from './renderer'
|
||||
|
@ -3,12 +3,12 @@ import {
|
||||
MethodOptions,
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithArrayProps,
|
||||
ComponentOptionsWithObjectProps,
|
||||
EmitsOptions
|
||||
} from './apiOptions'
|
||||
ComponentOptionsWithObjectProps
|
||||
} from './componentOptions'
|
||||
import { SetupContext, RenderFunction } from './component'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { ExtractPropTypes, ComponentPropsOptions } from './componentProps'
|
||||
import { EmitsOptions } from './componentEmits'
|
||||
import { isFunction } from '@vue/shared'
|
||||
import { VNodeProps } from './vnode'
|
||||
|
||||
|
@ -14,25 +14,24 @@ import {
|
||||
import { ComponentPropsOptions, resolveProps } from './componentProps'
|
||||
import { Slots, resolveSlots } from './componentSlots'
|
||||
import { warn } from './warning'
|
||||
import {
|
||||
ErrorCodes,
|
||||
callWithErrorHandling,
|
||||
callWithAsyncErrorHandling
|
||||
} from './errorHandling'
|
||||
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
|
||||
import { AppContext, createAppContext, AppConfig } from './apiCreateApp'
|
||||
import { Directive, validateDirectiveName } from './directives'
|
||||
import { applyOptions, ComponentOptions, EmitsOptions } from './apiOptions'
|
||||
import { applyOptions, ComponentOptions } from './componentOptions'
|
||||
import {
|
||||
EmitsOptions,
|
||||
ObjectEmitsOptions,
|
||||
EmitFn,
|
||||
emit
|
||||
} from './componentEmits'
|
||||
import {
|
||||
EMPTY_OBJ,
|
||||
isFunction,
|
||||
capitalize,
|
||||
NOOP,
|
||||
isObject,
|
||||
NO,
|
||||
makeMap,
|
||||
isPromise,
|
||||
isArray,
|
||||
hyphenate,
|
||||
ShapeFlags
|
||||
} from '@vue/shared'
|
||||
import { SuspenseBoundary } from './components/Suspense'
|
||||
@ -96,29 +95,10 @@ export const enum LifecycleHooks {
|
||||
ERROR_CAPTURED = 'ec'
|
||||
}
|
||||
|
||||
type UnionToIntersection<U> = (U extends any
|
||||
? (k: U) => void
|
||||
: never) extends ((k: infer I) => void)
|
||||
? I
|
||||
: never
|
||||
|
||||
export type Emit<
|
||||
Options = Record<string, any>,
|
||||
Event extends keyof Options = keyof Options
|
||||
> = Options extends any[]
|
||||
? (event: Options[0], ...args: any[]) => unknown[]
|
||||
: UnionToIntersection<
|
||||
{
|
||||
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
||||
? (event: key, ...args: Args) => unknown[]
|
||||
: (event: key, ...args: any[]) => unknown[]
|
||||
}[Event]
|
||||
>
|
||||
|
||||
export interface SetupContext<E = Record<string, any>> {
|
||||
export interface SetupContext<E = ObjectEmitsOptions> {
|
||||
attrs: Data
|
||||
slots: Slots
|
||||
emit: Emit<E>
|
||||
emit: EmitFn<E>
|
||||
}
|
||||
|
||||
export type RenderFunction = {
|
||||
@ -165,7 +145,7 @@ export interface ComponentInternalInstance {
|
||||
propsProxy: Data | null
|
||||
setupContext: SetupContext | null
|
||||
refs: Data
|
||||
emit: Emit
|
||||
emit: EmitFn
|
||||
|
||||
// suspense related
|
||||
suspense: SuspenseBoundary | null
|
||||
@ -268,29 +248,10 @@ export function createComponentInstance(
|
||||
rtg: null,
|
||||
rtc: null,
|
||||
ec: null,
|
||||
|
||||
emit: (event: string, ...args: any[]): any[] => {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
if (!handler && event.indexOf('update:') === 0) {
|
||||
event = hyphenate(event)
|
||||
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
}
|
||||
if (handler) {
|
||||
const res = callWithAsyncErrorHandling(
|
||||
handler,
|
||||
instance,
|
||||
ErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||
args
|
||||
)
|
||||
return isArray(res) ? res : [res]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
emit: null as any // to be set immediately
|
||||
}
|
||||
|
||||
instance.root = parent ? parent.root : instance
|
||||
instance.emit = emit.bind(null, instance)
|
||||
return instance
|
||||
}
|
||||
|
||||
|
93
packages/runtime-core/src/componentEmits.ts
Normal file
93
packages/runtime-core/src/componentEmits.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import {
|
||||
isArray,
|
||||
isOn,
|
||||
hasOwn,
|
||||
EMPTY_OBJ,
|
||||
capitalize,
|
||||
hyphenate
|
||||
} from '@vue/shared'
|
||||
import { ComponentInternalInstance } from './component'
|
||||
import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
|
||||
|
||||
export type ObjectEmitsOptions = Record<
|
||||
string,
|
||||
((...args: any[]) => any) | null
|
||||
>
|
||||
export type EmitsOptions = ObjectEmitsOptions | string[]
|
||||
|
||||
type UnionToIntersection<U> = (U extends any
|
||||
? (k: U) => void
|
||||
: never) extends ((k: infer I) => void)
|
||||
? I
|
||||
: never
|
||||
|
||||
export type EmitFn<
|
||||
Options = ObjectEmitsOptions,
|
||||
Event extends keyof Options = keyof Options
|
||||
> = Options extends any[]
|
||||
? (event: Options[0], ...args: any[]) => unknown[]
|
||||
: UnionToIntersection<
|
||||
{
|
||||
[key in Event]: Options[key] extends ((...args: infer Args) => any)
|
||||
? (event: key, ...args: Args) => unknown[]
|
||||
: (event: key, ...args: any[]) => unknown[]
|
||||
}[Event]
|
||||
>
|
||||
|
||||
export function emit(
|
||||
instance: ComponentInternalInstance,
|
||||
event: string,
|
||||
...args: any[]
|
||||
): any[] {
|
||||
const props = instance.vnode.props || EMPTY_OBJ
|
||||
let handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
// for v-model update:xxx events, also trigger kebab-case equivalent
|
||||
// for props passed via kebab-case
|
||||
if (!handler && event.indexOf('update:') === 0) {
|
||||
event = hyphenate(event)
|
||||
handler = props[`on${event}`] || props[`on${capitalize(event)}`]
|
||||
}
|
||||
if (handler) {
|
||||
const res = callWithAsyncErrorHandling(
|
||||
handler,
|
||||
instance,
|
||||
ErrorCodes.COMPONENT_EVENT_HANDLER,
|
||||
args
|
||||
)
|
||||
return isArray(res) ? res : [res]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeEmitsOptions(
|
||||
options: EmitsOptions | undefined
|
||||
): ObjectEmitsOptions | undefined {
|
||||
if (!options) {
|
||||
return
|
||||
} else if (isArray(options)) {
|
||||
if ((options as any)._n) {
|
||||
return (options as any)._n
|
||||
}
|
||||
const normalized: ObjectEmitsOptions = {}
|
||||
options.forEach(key => (normalized[key] = null))
|
||||
Object.defineProperty(options, '_n', { value: normalized })
|
||||
return normalized
|
||||
} else {
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
// Check if an incoming prop key is a declared emit event listener.
|
||||
// e.g. With `emits: { click: null }`, props named `onClick` and `onclick` are
|
||||
// both considered matched listeners.
|
||||
export function isEmitListener(
|
||||
emits: ObjectEmitsOptions,
|
||||
key: string
|
||||
): boolean {
|
||||
return (
|
||||
isOn(key) &&
|
||||
(hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
|
||||
hasOwn(emits, key.slice(2)))
|
||||
)
|
||||
}
|
@ -41,6 +41,7 @@ import {
|
||||
WritableComputedOptions
|
||||
} from '@vue/reactivity'
|
||||
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
|
||||
import { EmitsOptions } from './componentEmits'
|
||||
import { Directive } from './directives'
|
||||
import { ComponentPublicInstance } from './componentProxy'
|
||||
import { warn } from './warning'
|
||||
@ -149,8 +150,6 @@ export interface MethodOptions {
|
||||
[key: string]: Function
|
||||
}
|
||||
|
||||
export type EmitsOptions = Record<string, any> | string[]
|
||||
|
||||
export type ExtractComputedReturns<T extends any> = {
|
||||
[key in keyof T]: T[key] extends { get: Function }
|
||||
? ReturnType<T[key]['get']>
|
@ -14,12 +14,11 @@ import {
|
||||
makeMap,
|
||||
isReservedProp,
|
||||
EMPTY_ARR,
|
||||
ShapeFlags,
|
||||
isOn
|
||||
ShapeFlags
|
||||
} from '@vue/shared'
|
||||
import { warn } from './warning'
|
||||
import { Data, ComponentInternalInstance } from './component'
|
||||
import { EmitsOptions } from './apiOptions'
|
||||
import { normalizeEmitsOptions, isEmitListener } from './componentEmits'
|
||||
|
||||
export type ComponentPropsOptions<P = Data> =
|
||||
| ComponentObjectPropsOptions<P>
|
||||
@ -147,7 +146,7 @@ export function resolveProps(
|
||||
let camelKey
|
||||
if (hasDeclaredProps && hasOwn(options, (camelKey = camelize(key)))) {
|
||||
setProp(camelKey, value)
|
||||
} else if (!emits || !isListener(emits, key)) {
|
||||
} else if (!emits || !isEmitListener(emits, key)) {
|
||||
// Any non-declared (either as a prop or an emitted event) props are put
|
||||
// into a separate `attrs` object for spreading. Make sure to preserve
|
||||
// original key casing
|
||||
@ -281,35 +280,6 @@ export function normalizePropsOptions(
|
||||
return normalizedEntry
|
||||
}
|
||||
|
||||
function normalizeEmitsOptions(
|
||||
options: EmitsOptions | undefined
|
||||
): Record<string, any> | undefined {
|
||||
if (!options) {
|
||||
return
|
||||
} else if (isArray(options)) {
|
||||
if ((options as any)._n) {
|
||||
return (options as any)._n
|
||||
}
|
||||
const normalized: Record<string, null> = {}
|
||||
options.forEach(key => (normalized[key] = null))
|
||||
Object.defineProperty(options, '_n', normalized)
|
||||
return normalized
|
||||
} else {
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
function isListener(emits: Record<string, any>, key: string): boolean {
|
||||
if (!isOn(key)) {
|
||||
return false
|
||||
}
|
||||
const eventName = key.slice(2)
|
||||
return (
|
||||
hasOwn(emits, eventName) ||
|
||||
hasOwn(emits, eventName[0].toLowerCase() + eventName.slice(1))
|
||||
)
|
||||
}
|
||||
|
||||
// use function string name to check type constructors
|
||||
// so that it works across vms / iframes.
|
||||
function getType(ctor: Prop<any>): string {
|
||||
|
@ -1,23 +1,23 @@
|
||||
import { ComponentInternalInstance, Data, Emit } from './component'
|
||||
import { ComponentInternalInstance, Data } from './component'
|
||||
import { nextTick, queueJob } from './scheduler'
|
||||
import { instanceWatch } from './apiWatch'
|
||||
import { EMPTY_OBJ, hasOwn, isGloballyWhitelisted, NOOP } from '@vue/shared'
|
||||
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
||||
import {
|
||||
ExtractComputedReturns,
|
||||
ComponentOptionsBase,
|
||||
ComputedOptions,
|
||||
MethodOptions,
|
||||
resolveMergedOptions,
|
||||
EmitsOptions
|
||||
} from './apiOptions'
|
||||
import { ReactiveEffect, UnwrapRef } from '@vue/reactivity'
|
||||
import { warn } from './warning'
|
||||
resolveMergedOptions
|
||||
} from './componentOptions'
|
||||
import { normalizePropsOptions } from './componentProps'
|
||||
import { EmitsOptions, EmitFn } from './componentEmits'
|
||||
import { Slots } from './componentSlots'
|
||||
import {
|
||||
currentRenderingInstance,
|
||||
markAttrsAccessed
|
||||
} from './componentRenderUtils'
|
||||
import { normalizePropsOptions } from './componentProps'
|
||||
import { warn } from './warning'
|
||||
|
||||
// public properties exposed on the proxy, which is used as the render context
|
||||
// in templates (as `this` in the render option)
|
||||
@ -38,7 +38,7 @@ export type ComponentPublicInstance<
|
||||
$slots: Slots
|
||||
$root: ComponentInternalInstance | null
|
||||
$parent: ComponentInternalInstance | null
|
||||
$emit: Emit<E>
|
||||
$emit: EmitFn<E>
|
||||
$el: any
|
||||
$options: ComponentOptionsBase<P, B, D, C, M, E>
|
||||
$forceUpdate: ReactiveEffect
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
ComponentOptionsWithArrayProps,
|
||||
ComponentOptionsWithObjectProps,
|
||||
ComponentOptions
|
||||
} from './apiOptions'
|
||||
} from './componentOptions'
|
||||
import { ExtractPropTypes } from './componentProps'
|
||||
|
||||
// `h` is a more user-friendly version of `createVNode` that allows omitting the
|
||||
|
@ -186,7 +186,7 @@ export {
|
||||
ComponentOptionsWithoutProps,
|
||||
ComponentOptionsWithObjectProps as ComponentOptionsWithProps,
|
||||
ComponentOptionsWithArrayProps
|
||||
} from './apiOptions'
|
||||
} from './componentOptions'
|
||||
export { ComponentPublicInstance } from './componentProxy'
|
||||
export {
|
||||
Renderer,
|
||||
|
@ -6,8 +6,9 @@ const Foo = (props: { foo: number }) => props.foo
|
||||
|
||||
// TSX
|
||||
expectType<JSX.Element>(<Foo foo={1} />)
|
||||
expectError(<Foo />)
|
||||
// expectError(<Foo />) // tsd does not catch missing type errors
|
||||
expectError(<Foo foo="bar" />)
|
||||
expectError(<Foo baz="bar" />)
|
||||
|
||||
// Explicit signature with props + emits
|
||||
const Bar: FunctionalComponent<
|
||||
@ -35,5 +36,6 @@ expectError((Bar.emits = { baz: () => void 0 }))
|
||||
|
||||
// TSX
|
||||
expectType<JSX.Element>(<Bar foo={1} />)
|
||||
expectError(<Bar />)
|
||||
// expectError(<Foo />) // tsd does not catch missing type errors
|
||||
expectError(<Bar foo="bar" />)
|
||||
expectError(<Foo baz="bar" />)
|
||||
|
Loading…
Reference in New Issue
Block a user