refactor(runtime-core): extract component emit related logic into dedicated file

This commit is contained in:
Evan You 2020-04-03 19:08:17 -04:00
parent bf473a64ea
commit 24e9efcc21
10 changed files with 128 additions and 103 deletions

View File

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

View File

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

View File

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

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

View File

@ -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']>

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@ export {
ComponentOptionsWithoutProps,
ComponentOptionsWithObjectProps as ComponentOptionsWithProps,
ComponentOptionsWithArrayProps
} from './apiOptions'
} from './componentOptions'
export { ComponentPublicInstance } from './componentProxy'
export {
Renderer,

View File

@ -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" />)