]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
refactor(runtime-core): extract component emit related logic into dedicated file
authorEvan You <yyx990803@gmail.com>
Fri, 3 Apr 2020 23:08:17 +0000 (19:08 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 3 Apr 2020 23:08:17 +0000 (19:08 -0400)
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/apiDefineComponent.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts [new file with mode: 0644]
packages/runtime-core/src/componentOptions.ts [moved from packages/runtime-core/src/apiOptions.ts with 99% similarity]
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/h.ts
packages/runtime-core/src/index.ts
test-dts/functionalComponent.test-d.tsx

index 2f76d6b4ec396092fd3fbb1cbb0d7af38be4ef21..63d386168f4a61a6e466d10d686e403e38dc9a3c 100644 (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'
index 6d261f1b3f66047b567f6acd87fdebed64f41b26..083f0dd655a3c293635aa8e63d77ed1f6eae7d12 100644 (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'
 
index de40cdbfcc0ebfa6d7953fb3edc4ba91e7068f55..6b7fdb9c366a6c33b217336a6b3a66a4c87669df 100644 (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
 }
 
diff --git a/packages/runtime-core/src/componentEmits.ts b/packages/runtime-core/src/componentEmits.ts
new file mode 100644 (file)
index 0000000..b272065
--- /dev/null
@@ -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)))
+  )
+}
similarity index 99%
rename from packages/runtime-core/src/apiOptions.ts
rename to packages/runtime-core/src/componentOptions.ts
index 21df1f9607549f18b4ccae2a21d2781be2033900..d939731672467c9a9188b49098d94a50d9806b3a 100644 (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']>
index 1b51a9ebc780a845eb021ac395bedf0d2f93ab9b..eb746b367a084f7ea7fa26f4d205078deff8a7d3 100644 (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 {
index 5b0e0890e81b1128eb00d1c0b7c3f04bd3a66b62..ef46ffbfe0c9a509997a5cf5ddc87096d7576283 100644 (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
index ee091035a386a56397a3e3ca6b93b9df9a964b81..02e9bc8ce8a862e8ef08269b63d2f3e248fcfbb2 100644 (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
index eb5b037081c3a17d7bb6105b2f1927a879fac668..4cd04f4dbbfc3069c28ceef84bc0c0d0b1ab0b12 100644 (file)
@@ -186,7 +186,7 @@ export {
   ComponentOptionsWithoutProps,
   ComponentOptionsWithObjectProps as ComponentOptionsWithProps,
   ComponentOptionsWithArrayProps
-} from './apiOptions'
+} from './componentOptions'
 export { ComponentPublicInstance } from './componentProxy'
 export {
   Renderer,
index a5b4d899c6d97f1e26829a9133a4f53c067c671f..c95ae4a59ea7b3cc9ffb6e0b4da86f190876a433 100644 (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" />)