]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-core): fix props/emits resolving with global mixins
authorEvan You <yyx990803@gmail.com>
Mon, 31 Aug 2020 22:32:07 +0000 (18:32 -0400)
committerEvan You <yyx990803@gmail.com>
Mon, 31 Aug 2020 22:32:07 +0000 (18:32 -0400)
fix #1975

packages/runtime-core/__tests__/componentEmits.spec.ts
packages/runtime-core/__tests__/componentProps.spec.ts
packages/runtime-core/src/apiCreateApp.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentEmits.ts
packages/runtime-core/src/componentOptions.ts
packages/runtime-core/src/componentProps.ts
packages/runtime-core/src/componentPublicInstance.ts

index b47a16cb53382c108e98014121e69a07527b1f70..973099545e0ad87ad93370f921c61d63c0c1a830 100644 (file)
@@ -178,40 +178,13 @@ describe('component: emit', () => {
     expect(fn).toHaveBeenCalledTimes(1)
   })
 
-  describe('isEmitListener', () => {
-    test('array option', () => {
-      const def1 = { emits: ['click'] }
-      expect(isEmitListener(def1, 'onClick')).toBe(true)
-      expect(isEmitListener(def1, 'onclick')).toBe(false)
-      expect(isEmitListener(def1, 'onBlick')).toBe(false)
-    })
-
-    test('object option', () => {
-      const def2 = { emits: { click: null } }
-      expect(isEmitListener(def2, 'onClick')).toBe(true)
-      expect(isEmitListener(def2, 'onclick')).toBe(false)
-      expect(isEmitListener(def2, 'onBlick')).toBe(false)
-    })
-
-    test('with mixins and extends', () => {
-      const mixin1 = { emits: ['foo'] }
-      const mixin2 = { emits: ['bar'] }
-      const extend = { emits: ['baz'] }
-      const def3 = {
-        mixins: [mixin1, mixin2],
-        extends: extend
-      }
-      expect(isEmitListener(def3, 'onFoo')).toBe(true)
-      expect(isEmitListener(def3, 'onBar')).toBe(true)
-      expect(isEmitListener(def3, 'onBaz')).toBe(true)
-      expect(isEmitListener(def3, 'onclick')).toBe(false)
-      expect(isEmitListener(def3, 'onBlick')).toBe(false)
-    })
-
-    test('.once listeners', () => {
-      const def2 = { emits: { click: null } }
-      expect(isEmitListener(def2, 'onClickOnce')).toBe(true)
-      expect(isEmitListener(def2, 'onclickOnce')).toBe(false)
-    })
+  test('isEmitListener', () => {
+    const options = { click: null }
+    expect(isEmitListener(options, 'onClick')).toBe(true)
+    expect(isEmitListener(options, 'onclick')).toBe(false)
+    expect(isEmitListener(options, 'onBlick')).toBe(false)
+    // .once listeners
+    expect(isEmitListener(options, 'onClickOnce')).toBe(true)
+    expect(isEmitListener(options, 'onclickOnce')).toBe(false)
   })
 })
index dee736c4620599b3a5a00419f50f54052d7bcaf8..d0ddc5c1fdc2b00cc693b5816825fd1c1565db40 100644 (file)
@@ -7,7 +7,8 @@ import {
   FunctionalComponent,
   defineComponent,
   ref,
-  serializeInner
+  serializeInner,
+  createApp
 } from '@vue/runtime-test'
 import { render as domRender, nextTick } from 'vue'
 
@@ -309,4 +310,44 @@ describe('component props', () => {
     expect(setupProps).toMatchObject(props)
     expect(renderProxy.$props).toMatchObject(props)
   })
+
+  test('merging props from global mixins', () => {
+    let setupProps: any
+    let renderProxy: any
+
+    const M1 = {
+      props: ['m1']
+    }
+    const M2 = {
+      props: { m2: null }
+    }
+    const Comp = {
+      props: ['self'],
+      setup(props: any) {
+        setupProps = props
+      },
+      render(this: any) {
+        renderProxy = this
+        return h('div', [this.self, this.m1, this.m2])
+      }
+    }
+
+    const props = {
+      self: 'from self, ',
+      m1: 'from mixin 1, ',
+      m2: 'from mixin 2'
+    }
+    const app = createApp(Comp, props)
+    app.mixin(M1)
+    app.mixin(M2)
+
+    const root = nodeOps.createElement('div')
+    app.mount(root)
+
+    expect(serializeInner(root)).toMatch(
+      `from self, from mixin 1, from mixin 2`
+    )
+    expect(setupProps).toMatchObject(props)
+    expect(renderProxy.$props).toMatchObject(props)
+  })
 })
index 39140c31571c0843742a48bab9debe02c8605210..2397cdf337980f48cbd2dc723cba43e3d190ed23 100644 (file)
@@ -33,6 +33,7 @@ export interface App<HostElement = any> {
   provide<T>(key: InjectionKey<T> | string, value: T): this
 
   // internal, but we need to expose these for the server-renderer and devtools
+  _uid: number
   _component: ConcreteComponent
   _props: Data | null
   _container: HostElement | null
@@ -108,6 +109,8 @@ export type CreateAppFunction<HostElement> = (
   rootProps?: Data | null
 ) => App<HostElement>
 
+let uid = 0
+
 export function createAppAPI<HostElement>(
   render: RootRenderFunction,
   hydrate?: RootHydrateFunction
@@ -124,6 +127,7 @@ export function createAppAPI<HostElement>(
     let isMounted = false
 
     const app: App = (context.app = {
+      _uid: uid++,
       _component: rootComponent as ConcreteComponent,
       _props: rootProps,
       _container: null,
index 0bc928911e5b0224f96d86b84e569c1fdeda483e..1182447e8e6ee6f84635b7fcbd3dba8d6578afdc 100644 (file)
@@ -18,7 +18,8 @@ import {
 import {
   ComponentPropsOptions,
   NormalizedPropsOptions,
-  initProps
+  initProps,
+  normalizePropsOptions
 } from './componentProps'
 import { Slots, initSlots, InternalSlots } from './componentSlots'
 import { warn } from './warning'
@@ -30,7 +31,8 @@ import {
   EmitsOptions,
   ObjectEmitsOptions,
   EmitFn,
-  emit
+  emit,
+  normalizeEmitsOptions
 } from './componentEmits'
 import {
   EMPTY_OBJ,
@@ -72,11 +74,11 @@ export interface ComponentInternalOptions {
   /**
    * @internal
    */
-  __props?: NormalizedPropsOptions | []
+  __props?: Record<number, NormalizedPropsOptions>
   /**
    * @internal
    */
-  __emits?: ObjectEmitsOptions
+  __emits?: Record<number, ObjectEmitsOptions | null>
   /**
    * @internal
    */
@@ -231,6 +233,16 @@ export interface ComponentInternalInstance {
    * @internal
    */
   directives: Record<string, Directive> | null
+  /**
+   * reoslved props options
+   * @internal
+   */
+  propsOptions: NormalizedPropsOptions
+  /**
+   * resolved emits options
+   * @internal
+   */
+  emitsOptions: ObjectEmitsOptions | null
 
   // the rest are only for stateful components ---------------------------------
 
@@ -254,14 +266,17 @@ export interface ComponentInternalInstance {
    */
   ctx: Data
 
-  // internal state
+  // state
   data: Data
   props: Data
   attrs: Data
   slots: InternalSlots
   refs: Data
   emit: EmitFn
-  // used for keeping track of .once event handlers on components
+  /**
+   * used for keeping track of .once event handlers on components
+   * @internal
+   */
   emitted: Record<string, boolean> | null
 
   /**
@@ -387,6 +402,14 @@ export function createComponentInstance(
     components: null,
     directives: null,
 
+    // resolved props and emits options
+    propsOptions: normalizePropsOptions(type, appContext),
+    emitsOptions: normalizeEmitsOptions(type, appContext),
+
+    // emit
+    emit: null as any, // to be set immediately
+    emitted: null,
+
     // state
     ctx: EMPTY_OBJ,
     data: EMPTY_OBJ,
@@ -419,9 +442,7 @@ export function createComponentInstance(
     a: null,
     rtg: null,
     rtc: null,
-    ec: null,
-    emit: null as any, // to be set immediately
-    emitted: null
+    ec: null
   }
   if (__DEV__) {
     instance.ctx = createRenderContext(instance)
index 33ca7f69458e09b29e0bff71622f8fcf9b4f661f..6418f8c7d5d6ba8508560cc5f1071a4dd604f8fd 100644 (file)
@@ -8,12 +8,16 @@ import {
   isFunction,
   extend
 } from '@vue/shared'
-import { ComponentInternalInstance, ConcreteComponent } from './component'
+import {
+  ComponentInternalInstance,
+  ComponentOptions,
+  ConcreteComponent
+} from './component'
 import { callWithAsyncErrorHandling, ErrorCodes } from './errorHandling'
 import { warn } from './warning'
-import { normalizePropsOptions } from './componentProps'
 import { UnionToIntersection } from './helpers/typeUtils'
 import { devtoolsComponentEmit } from './devtools'
+import { AppContext } from './apiCreateApp'
 
 export type ObjectEmitsOptions = Record<
   string,
@@ -44,10 +48,12 @@ export function emit(
   const props = instance.vnode.props || EMPTY_OBJ
 
   if (__DEV__) {
-    const options = normalizeEmitsOptions(instance.type)
-    if (options) {
-      if (!(event in options)) {
-        const propsOptions = normalizePropsOptions(instance.type)[0]
+    const {
+      emitsOptions,
+      propsOptions: [propsOptions]
+    } = instance
+    if (emitsOptions) {
+      if (!(event in emitsOptions)) {
         if (!propsOptions || !(`on` + capitalize(event) in propsOptions)) {
           warn(
             `Component emitted event "${event}" but it is neither declared in ` +
@@ -55,7 +61,7 @@ export function emit(
           )
         }
       } else {
-        const validator = options[event]
+        const validator = emitsOptions[event]
         if (isFunction(validator)) {
           const isValid = validator(...args)
           if (!isValid) {
@@ -98,11 +104,16 @@ export function emit(
   }
 }
 
-function normalizeEmitsOptions(
-  comp: ConcreteComponent
-): ObjectEmitsOptions | undefined {
-  if (hasOwn(comp, '__emits')) {
-    return comp.__emits
+export function normalizeEmitsOptions(
+  comp: ConcreteComponent,
+  appContext: AppContext,
+  asMixin = false
+): ObjectEmitsOptions | null {
+  const appId = appContext.app ? appContext.app._uid : -1
+  const cache = comp.__emits || (comp.__emits = {})
+  const cached = cache[appId]
+  if (cached !== undefined) {
+    return cached
   }
 
   const raw = comp.emits
@@ -111,18 +122,23 @@ function normalizeEmitsOptions(
   // apply mixin/extends props
   let hasExtends = false
   if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
-    if (comp.extends) {
+    const extendEmits = (raw: ComponentOptions) => {
       hasExtends = true
-      extend(normalized, normalizeEmitsOptions(comp.extends))
+      extend(normalized, normalizeEmitsOptions(raw, appContext, true))
+    }
+    if (!asMixin && appContext.mixins.length) {
+      appContext.mixins.forEach(extendEmits)
+    }
+    if (comp.extends) {
+      extendEmits(comp.extends)
     }
     if (comp.mixins) {
-      hasExtends = true
-      comp.mixins.forEach(m => extend(normalized, normalizeEmitsOptions(m)))
+      comp.mixins.forEach(extendEmits)
     }
   }
 
   if (!raw && !hasExtends) {
-    return (comp.__emits = undefined)
+    return (cache[appId] = null)
   }
 
   if (isArray(raw)) {
@@ -130,20 +146,22 @@ function normalizeEmitsOptions(
   } else {
     extend(normalized, raw)
   }
-  return (comp.__emits = normalized)
+  return (cache[appId] = normalized)
 }
 
 // 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(comp: ConcreteComponent, key: string): boolean {
-  let emits: ObjectEmitsOptions | undefined
-  if (!isOn(key) || !(emits = normalizeEmitsOptions(comp))) {
+export function isEmitListener(
+  options: ObjectEmitsOptions | null,
+  key: string
+): boolean {
+  if (!options || !isOn(key)) {
     return false
   }
   key = key.replace(/Once$/, '')
   return (
-    hasOwn(emits, key[2].toLowerCase() + key.slice(3)) ||
-    hasOwn(emits, key.slice(2))
+    hasOwn(options, key[2].toLowerCase() + key.slice(3)) ||
+    hasOwn(options, key.slice(2))
   )
 }
index d2a4c05944f49dd2e65a5447c92e72eca7d33e6b..29a09bf1b424b7942a888c7ce84897dc6bcead5b 100644 (file)
@@ -42,11 +42,7 @@ import {
   WritableComputedOptions,
   toRaw
 } from '@vue/reactivity'
-import {
-  ComponentObjectPropsOptions,
-  ExtractPropTypes,
-  normalizePropsOptions
-} from './componentProps'
+import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
 import { EmitsOptions } from './componentEmits'
 import { Directive } from './directives'
 import {
@@ -431,7 +427,7 @@ export function applyOptions(
   const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
 
   if (__DEV__) {
-    const propsOptions = normalizePropsOptions(options)[0]
+    const [propsOptions] = instance.propsOptions
     if (propsOptions) {
       for (const key in propsOptions) {
         checkDuplicateProperties!(OptionTypes.PROPS, key)
index 28d7de2a5e727429e9457796029313aa8129ea8a..a8bccd9a5afba68011c0f121391a8268c02fc727 100644 (file)
@@ -31,6 +31,7 @@ import {
 } from './component'
 import { isEmitListener } from './componentEmits'
 import { InternalObjectKey } from './vnode'
+import { AppContext } from './apiCreateApp'
 
 export type ComponentPropsOptions<P = Data> =
   | ComponentObjectPropsOptions<P>
@@ -107,7 +108,8 @@ type NormalizedProp =
 
 // normalized value is a tuple of the actual normalized options
 // and an array of prop keys that need value casting (booleans and defaults)
-export type NormalizedPropsOptions = [Record<string, NormalizedProp>, string[]]
+export type NormalizedProps = Record<string, NormalizedProp>
+export type NormalizedPropsOptions = [NormalizedProps, string[]] | []
 
 export function initProps(
   instance: ComponentInternalInstance,
@@ -121,7 +123,7 @@ export function initProps(
   setFullProps(instance, rawProps, props, attrs)
   // validation
   if (__DEV__) {
-    validateProps(props, instance.type)
+    validateProps(props, instance)
   }
 
   if (isStateful) {
@@ -151,7 +153,7 @@ export function updateProps(
     vnode: { patchFlag }
   } = instance
   const rawCurrentProps = toRaw(props)
-  const [options] = normalizePropsOptions(instance.type)
+  const [options] = instance.propsOptions
 
   if (
     // always force full diff if hmr is enabled
@@ -236,7 +238,7 @@ export function updateProps(
   trigger(instance, TriggerOpTypes.SET, '$attrs')
 
   if (__DEV__ && rawProps) {
-    validateProps(props, instance.type)
+    validateProps(props, instance)
   }
 }
 
@@ -246,7 +248,7 @@ function setFullProps(
   props: Data,
   attrs: Data
 ) {
-  const [options, needCastKeys] = normalizePropsOptions(instance.type)
+  const [options, needCastKeys] = instance.propsOptions
   if (rawProps) {
     for (const key in rawProps) {
       const value = rawProps[key]
@@ -259,7 +261,7 @@ function setFullProps(
       let camelKey
       if (options && hasOwn(options, (camelKey = camelize(key)))) {
         props[camelKey] = value
-      } else if (!isEmitListener(instance.type, key)) {
+      } else if (!isEmitListener(instance.emitsOptions, 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
@@ -283,7 +285,7 @@ function setFullProps(
 }
 
 function resolvePropValue(
-  options: NormalizedPropsOptions[0],
+  options: NormalizedProps,
   props: Data,
   key: string,
   value: unknown
@@ -315,10 +317,15 @@ function resolvePropValue(
 }
 
 export function normalizePropsOptions(
-  comp: ConcreteComponent
-): NormalizedPropsOptions | [] {
-  if (comp.__props) {
-    return comp.__props
+  comp: ConcreteComponent,
+  appContext: AppContext,
+  asMixin = false
+): NormalizedPropsOptions {
+  const appId = appContext.app ? appContext.app._uid : -1
+  const cache = comp.__props || (comp.__props = {})
+  const cached = cache[appId]
+  if (cached) {
+    return cached
   }
 
   const raw = comp.props
@@ -329,22 +336,24 @@ export function normalizePropsOptions(
   let hasExtends = false
   if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
     const extendProps = (raw: ComponentOptions) => {
-      const [props, keys] = normalizePropsOptions(raw)
+      hasExtends = true
+      const [props, keys] = normalizePropsOptions(raw, appContext, true)
       extend(normalized, props)
       if (keys) needCastKeys.push(...keys)
     }
+    if (!asMixin && appContext.mixins.length) {
+      appContext.mixins.forEach(extendProps)
+    }
     if (comp.extends) {
-      hasExtends = true
       extendProps(comp.extends)
     }
     if (comp.mixins) {
-      hasExtends = true
       comp.mixins.forEach(extendProps)
     }
   }
 
   if (!raw && !hasExtends) {
-    return (comp.__props = EMPTY_ARR)
+    return (cache[appId] = EMPTY_ARR)
   }
 
   if (isArray(raw)) {
@@ -381,9 +390,8 @@ export function normalizePropsOptions(
       }
     }
   }
-  const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
-  comp.__props = normalizedEntry
-  return normalizedEntry
+
+  return (cache[appId] = [normalized, needCastKeys])
 }
 
 // use function string name to check type constructors
@@ -416,9 +424,9 @@ function getTypeIndex(
 /**
  * dev only
  */
-function validateProps(props: Data, comp: ConcreteComponent) {
+function validateProps(props: Data, instance: ComponentInternalInstance) {
   const rawValues = toRaw(props)
-  const options = normalizePropsOptions(comp)[0]
+  const options = instance.propsOptions[0]
   for (const key in options) {
     let opt = options[key]
     if (opt == null) continue
index 2881452715037d0b9f743e7e826e86ac1f73365d..35125ac819a95f4ee6fb4b23cbe399656a1d23b4 100644 (file)
@@ -29,7 +29,6 @@ import {
   resolveMergedOptions,
   isInBeforeCreate
 } from './componentOptions'
-import { normalizePropsOptions } from './componentProps'
 import { EmitsOptions, EmitFn } from './componentEmits'
 import { Slots } from './componentSlots'
 import {
@@ -250,7 +249,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       } else if (
         // only cache other properties when instance has declared (thus stable)
         // props
-        (normalizedProps = normalizePropsOptions(type)[0]) &&
+        (normalizedProps = instance.propsOptions[0]) &&
         hasOwn(normalizedProps, key)
       ) {
         accessCache![key] = AccessTypes.PROPS
@@ -354,7 +353,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
 
   has(
     {
-      _: { data, setupState, accessCache, ctx, type, appContext }
+      _: { data, setupState, accessCache, ctx, appContext, propsOptions }
     }: ComponentRenderContext,
     key: string
   ) {
@@ -363,8 +362,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
       accessCache![key] !== undefined ||
       (data !== EMPTY_OBJ && hasOwn(data, key)) ||
       (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
-      ((normalizedProps = normalizePropsOptions(type)[0]) &&
-        hasOwn(normalizedProps, key)) ||
+      ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
       hasOwn(ctx, key) ||
       hasOwn(publicPropertiesMap, key) ||
       hasOwn(appContext.config.globalProperties, key)
@@ -450,8 +448,10 @@ export function createRenderContext(instance: ComponentInternalInstance) {
 export function exposePropsOnRenderContext(
   instance: ComponentInternalInstance
 ) {
-  const { ctx, type } = instance
-  const propsOptions = normalizePropsOptions(type)[0]
+  const {
+    ctx,
+    propsOptions: [propsOptions]
+  } = instance
   if (propsOptions) {
     Object.keys(propsOptions).forEach(key => {
       Object.defineProperty(ctx, key, {