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, toRaw, shallowReadonly, ReactiveFlags } from '@vue/reactivity' import { ExtractComputedReturns, ComponentOptionsBase, ComputedOptions, MethodOptions, resolveMergedOptions } from './componentOptions' import { normalizePropsOptions } from './componentProps' import { EmitsOptions, EmitFn } from './componentEmits' import { Slots } from './componentSlots' import { currentRenderingInstance, markAttrsAccessed } from './componentRenderUtils' import { warn } from './warning' /** * Custom properties added to component instances in any way and can be accessed through `this` * * @example * Here is an example of adding a property `$router` to every component instance: * ```ts * import { createApp } from 'vue' * import { Router, createRouter } from 'vue-router' * * declare module '@vue/runtime-core' { * interface ComponentCustomProperties { * $router: Router * } * } * * // effectively adding the router to every component instance * const app = createApp({}) * const router = createRouter() * app.config.globalProperties.$router = router * * const vm = app.mount('#app') * // we can access the router from the instance * vm.$router.push('/') * ``` */ export interface ComponentCustomProperties {} // public properties exposed on the proxy, which is used as the render context // in templates (as `this` in the render option) export type ComponentPublicInstance< P = {}, // props type extracted from props option B = {}, // raw bindings returned from setup() D = {}, // return from data() C extends ComputedOptions = {}, M extends MethodOptions = {}, E extends EmitsOptions = {}, PublicProps = P > = { $: ComponentInternalInstance $data: D $props: PublicProps $attrs: Data $refs: Data $slots: Slots $root: ComponentPublicInstance | null $parent: ComponentPublicInstance | null $emit: EmitFn $el: any $options: ComponentOptionsBase $forceUpdate: ReactiveEffect $nextTick: typeof nextTick $watch: typeof instanceWatch } & P & UnwrapRef & D & ExtractComputedReturns & M & ComponentCustomProperties const publicPropertiesMap: Record< string, (i: ComponentInternalInstance) => any > = { $: i => i, $el: i => i.vnode.el, $data: i => i.data, $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props), $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs), $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots), $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs), $parent: i => i.parent && i.parent.proxy, $root: i => i.root && i.root.proxy, $emit: i => i.emit, $options: i => (__FEATURE_OPTIONS__ ? resolveMergedOptions(i) : i.type), $forceUpdate: i => () => queueJob(i.update), $nextTick: () => nextTick, $watch: __FEATURE_OPTIONS__ ? i => instanceWatch.bind(i) : NOOP } const enum AccessTypes { SETUP, DATA, PROPS, CONTEXT, OTHER } export interface ComponentRenderContext { [key: string]: any _: ComponentInternalInstance } export const PublicInstanceProxyHandlers: ProxyHandler = { get({ _: instance }: ComponentRenderContext, key: string) { const { ctx, setupState, data, props, accessCache, type, appContext } = instance // let @vue/reatvitiy know it should never observe Vue public instances. if (key === ReactiveFlags.skip) { return true } // data / props / ctx // This getter gets called for every property access on the render context // during render and is a major hotspot. The most expensive part of this // is the multiple hasOwn() calls. It's much faster to do a simple property // access on a plain object, so we use an accessCache object (with null // prototype) to memoize what access type a key corresponds to. if (key[0] !== '$') { const n = accessCache![key] if (n !== undefined) { switch (n) { case AccessTypes.SETUP: return setupState[key] case AccessTypes.DATA: return data[key] case AccessTypes.CONTEXT: return ctx[key] case AccessTypes.PROPS: return props![key] // default: just fallthrough } } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { accessCache![key] = AccessTypes.SETUP return setupState[key] } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { accessCache![key] = AccessTypes.DATA return data[key] } else if ( // only cache other properties when instance has declared (thus stable) // props type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key) ) { accessCache![key] = AccessTypes.PROPS return props![key] } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { accessCache![key] = AccessTypes.CONTEXT return ctx[key] } else { accessCache![key] = AccessTypes.OTHER } } const publicGetter = publicPropertiesMap[key] let cssModule, globalProperties // public $xxx properties if (publicGetter) { if (__DEV__ && key === '$attrs') { markAttrsAccessed() } return publicGetter(instance) } else if ( // css module (injected by vue-loader) (cssModule = type.__cssModules) && (cssModule = cssModule[key]) ) { return cssModule } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) { // user may set custom properties to `this` that start with `$` accessCache![key] = AccessTypes.CONTEXT return ctx[key] } else if ( // global properties ((globalProperties = appContext.config.globalProperties), hasOwn(globalProperties, key)) ) { return globalProperties[key] } else if ( __DEV__ && currentRenderingInstance && // #1091 avoid internal isRef/isVNode checks on component instance leading // to infinite warning loop key.indexOf('__v') !== 0 ) { if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) { warn( `Property ${JSON.stringify( key )} must be accessed via $data because it starts with a reserved ` + `character and is not proxied on the render context.` ) } else { warn( `Property ${JSON.stringify(key)} was accessed during render ` + `but is not defined on instance.` ) } } }, set( { _: instance }: ComponentRenderContext, key: string, value: any ): boolean { const { data, setupState, ctx } = instance if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) { setupState[key] = value } else if (data !== EMPTY_OBJ && hasOwn(data, key)) { data[key] = value } else if (key in instance.props) { __DEV__ && warn( `Attempting to mutate prop "${key}". Props are readonly.`, instance ) return false } if (key[0] === '$' && key.slice(1) in instance) { __DEV__ && warn( `Attempting to mutate public property "${key}". ` + `Properties starting with $ are reserved and readonly.`, instance ) return false } else { if (__DEV__ && key in instance.appContext.config.globalProperties) { Object.defineProperty(ctx, key, { enumerable: true, configurable: true, value }) } else { ctx[key] = value } } return true }, has( { _: { data, setupState, accessCache, ctx, type, appContext } }: ComponentRenderContext, key: string ) { return ( accessCache![key] !== undefined || (data !== EMPTY_OBJ && hasOwn(data, key)) || (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) || (type.props && hasOwn(normalizePropsOptions(type.props)[0]!, key)) || hasOwn(ctx, key) || hasOwn(publicPropertiesMap, key) || hasOwn(appContext.config.globalProperties, key) ) } } if (__DEV__ && !__TEST__) { PublicInstanceProxyHandlers.ownKeys = (target: ComponentRenderContext) => { warn( `Avoid app logic that relies on enumerating keys on a component instance. ` + `The keys will be empty in production mode to avoid performance overhead.` ) return Reflect.ownKeys(target) } } export const RuntimeCompiledPublicInstanceProxyHandlers = { ...PublicInstanceProxyHandlers, get(target: ComponentRenderContext, key: string) { // fast path for unscopables when using `with` block if ((key as any) === Symbol.unscopables) { return } return PublicInstanceProxyHandlers.get!(target, key, target) }, has(_: ComponentRenderContext, key: string) { const has = key[0] !== '_' && !isGloballyWhitelisted(key) if (__DEV__ && !has && PublicInstanceProxyHandlers.has!(_, key)) { warn( `Property ${JSON.stringify( key )} should not start with _ which is a reserved prefix for Vue internals.` ) } return has } } // In dev mode, the proxy target exposes the same properties as seen on `this` // for easier console inspection. In prod mode it will be an empty object so // these properties definitions can be skipped. export function createRenderContext(instance: ComponentInternalInstance) { const target: Record = {} // expose internal instance for proxy handlers Object.defineProperty(target, `_`, { configurable: true, enumerable: false, get: () => instance }) // expose public properties Object.keys(publicPropertiesMap).forEach(key => { Object.defineProperty(target, key, { configurable: true, enumerable: false, get: () => publicPropertiesMap[key](instance), // intercepted by the proxy so no need for implementation, // but needed to prevent set errors set: NOOP }) }) // expose global properties const { globalProperties } = instance.appContext.config Object.keys(globalProperties).forEach(key => { Object.defineProperty(target, key, { configurable: true, enumerable: false, get: () => globalProperties[key], set: NOOP }) }) return target as ComponentRenderContext } // dev only export function exposePropsOnRenderContext( instance: ComponentInternalInstance ) { const { ctx, type: { props: propsOptions } } = instance if (propsOptions) { Object.keys(normalizePropsOptions(propsOptions)[0]!).forEach(key => { Object.defineProperty(ctx, key, { enumerable: true, configurable: true, get: () => instance.props[key], set: NOOP }) }) } } // dev only export function exposeSetupStateOnRenderContext( instance: ComponentInternalInstance ) { const { ctx, setupState } = instance Object.keys(toRaw(setupState)).forEach(key => { Object.defineProperty(ctx, key, { enumerable: true, configurable: true, get: () => setupState[key], set: NOOP }) }) }