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)
})
})
FunctionalComponent,
defineComponent,
ref,
- serializeInner
+ serializeInner,
+ createApp
} from '@vue/runtime-test'
import { render as domRender, nextTick } from 'vue'
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)
+ })
})
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
rootProps?: Data | null
) => App<HostElement>
+let uid = 0
+
export function createAppAPI<HostElement>(
render: RootRenderFunction,
hydrate?: RootHydrateFunction
let isMounted = false
const app: App = (context.app = {
+ _uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
import {
ComponentPropsOptions,
NormalizedPropsOptions,
- initProps
+ initProps,
+ normalizePropsOptions
} from './componentProps'
import { Slots, initSlots, InternalSlots } from './componentSlots'
import { warn } from './warning'
EmitsOptions,
ObjectEmitsOptions,
EmitFn,
- emit
+ emit,
+ normalizeEmitsOptions
} from './componentEmits'
import {
EMPTY_OBJ,
/**
* @internal
*/
- __props?: NormalizedPropsOptions | []
+ __props?: Record<number, NormalizedPropsOptions>
/**
* @internal
*/
- __emits?: ObjectEmitsOptions
+ __emits?: Record<number, ObjectEmitsOptions | null>
/**
* @internal
*/
* @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 ---------------------------------
*/
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
/**
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,
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)
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,
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 ` +
)
}
} else {
- const validator = options[event]
+ const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...args)
if (!isValid) {
}
}
-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
// 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)) {
} 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))
)
}
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 {
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)
} from './component'
import { isEmitListener } from './componentEmits'
import { InternalObjectKey } from './vnode'
+import { AppContext } from './apiCreateApp'
export type ComponentPropsOptions<P = Data> =
| ComponentObjectPropsOptions<P>
// 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,
setFullProps(instance, rawProps, props, attrs)
// validation
if (__DEV__) {
- validateProps(props, instance.type)
+ validateProps(props, instance)
}
if (isStateful) {
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
trigger(instance, TriggerOpTypes.SET, '$attrs')
if (__DEV__ && rawProps) {
- validateProps(props, instance.type)
+ validateProps(props, instance)
}
}
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]
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
}
function resolvePropValue(
- options: NormalizedPropsOptions[0],
+ options: NormalizedProps,
props: Data,
key: string,
value: unknown
}
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
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)) {
}
}
}
- const normalizedEntry: NormalizedPropsOptions = [normalized, needCastKeys]
- comp.__props = normalizedEntry
- return normalizedEntry
+
+ return (cache[appId] = [normalized, needCastKeys])
}
// use function string name to check type constructors
/**
* 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
resolveMergedOptions,
isInBeforeCreate
} from './componentOptions'
-import { normalizePropsOptions } from './componentProps'
import { EmitsOptions, EmitFn } from './componentEmits'
import { Slots } from './componentSlots'
import {
} 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
has(
{
- _: { data, setupState, accessCache, ctx, type, appContext }
+ _: { data, setupState, accessCache, ctx, appContext, propsOptions }
}: ComponentRenderContext,
key: string
) {
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)
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, {