From: Evan You Date: Fri, 3 Apr 2020 23:08:17 +0000 (-0400) Subject: refactor(runtime-core): extract component emit related logic into dedicated file X-Git-Tag: v3.0.0-alpha.11~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=24e9efcc21dd517fddf8251873a06d71830e46eb;p=thirdparty%2Fvuejs%2Fcore.git refactor(runtime-core): extract component emit related logic into dedicated file --- diff --git a/packages/runtime-core/src/apiCreateApp.ts b/packages/runtime-core/src/apiCreateApp.ts index 2f76d6b4ec..63d386168f 100644 --- a/packages/runtime-core/src/apiCreateApp.ts +++ b/packages/runtime-core/src/apiCreateApp.ts @@ -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' diff --git a/packages/runtime-core/src/apiDefineComponent.ts b/packages/runtime-core/src/apiDefineComponent.ts index 6d261f1b3f..083f0dd655 100644 --- a/packages/runtime-core/src/apiDefineComponent.ts +++ b/packages/runtime-core/src/apiDefineComponent.ts @@ -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' diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index de40cdbfcc..6b7fdb9c36 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -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 extends any - ? (k: U) => void - : never) extends ((k: infer I) => void) - ? I - : never - -export type Emit< - Options = Record, - 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> { +export interface SetupContext { attrs: Data slots: Slots - emit: Emit + emit: EmitFn } 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 } diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts new file mode 100644 index 0000000000..b2720653c8 --- /dev/null +++ b/packages/runtime-core/src/componentEmits.ts @@ -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 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))) + ) +} diff --git a/packages/runtime-core/src/apiOptions.ts b/packages/runtime-core/src/componentOptions.ts similarity index 99% rename from packages/runtime-core/src/apiOptions.ts rename to packages/runtime-core/src/componentOptions.ts index 21df1f9607..d939731672 100644 --- a/packages/runtime-core/src/apiOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -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[] - export type ExtractComputedReturns = { [key in keyof T]: T[key] extends { get: Function } ? ReturnType diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index 1b51a9ebc7..eb746b367a 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/packages/runtime-core/src/componentProps.ts @@ -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

= | ComponentObjectPropsOptions

@@ -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 | undefined { - if (!options) { - return - } else if (isArray(options)) { - if ((options as any)._n) { - return (options as any)._n - } - const normalized: Record = {} - options.forEach(key => (normalized[key] = null)) - Object.defineProperty(options, '_n', normalized) - return normalized - } else { - return options - } -} - -function isListener(emits: Record, 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): string { diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 5b0e0890e8..ef46ffbfe0 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -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 + $emit: EmitFn $el: any $options: ComponentOptionsBase $forceUpdate: ReactiveEffect diff --git a/packages/runtime-core/src/h.ts b/packages/runtime-core/src/h.ts index ee091035a3..02e9bc8ce8 100644 --- a/packages/runtime-core/src/h.ts +++ b/packages/runtime-core/src/h.ts @@ -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 diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index eb5b037081..4cd04f4dbb 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -186,7 +186,7 @@ export { ComponentOptionsWithoutProps, ComponentOptionsWithObjectProps as ComponentOptionsWithProps, ComponentOptionsWithArrayProps -} from './apiOptions' +} from './componentOptions' export { ComponentPublicInstance } from './componentProxy' export { Renderer, diff --git a/test-dts/functionalComponent.test-d.tsx b/test-dts/functionalComponent.test-d.tsx index a5b4d899c6..c95ae4a59e 100644 --- a/test-dts/functionalComponent.test-d.tsx +++ b/test-dts/functionalComponent.test-d.tsx @@ -6,8 +6,9 @@ const Foo = (props: { foo: number }) => props.foo // TSX expectType() -expectError() +// expectError() // tsd does not catch missing type errors expectError() +expectError() // Explicit signature with props + emits const Bar: FunctionalComponent< @@ -35,5 +36,6 @@ expectError((Bar.emits = { baz: () => void 0 })) // TSX expectType() -expectError() +// expectError() // tsd does not catch missing type errors expectError() +expectError()