vue3-core/packages/runtime-core/src/componentPublicInstance.ts

609 lines
17 KiB
TypeScript

import {
type ComponentInternalInstance,
type Data,
getExposeProxy,
isStatefulComponent,
} from './component'
import { nextTick, queueJob } from './scheduler'
import {
type WatchOptions,
type WatchStopHandle,
instanceWatch,
} from './apiWatch'
import {
EMPTY_OBJ,
type IfAny,
NOOP,
type Prettify,
type UnionToIntersection,
extend,
hasOwn,
isFunction,
isGloballyAllowed,
isString,
} from '@vue/shared'
import {
type ShallowUnwrapRef,
TrackOpTypes,
type UnwrapNestedRefs,
shallowReadonly,
toRaw,
track,
} from '@vue/reactivity'
import {
type ComponentInjectOptions,
type ComponentOptionsBase,
type ComponentOptionsMixin,
type ComputedOptions,
type ExtractComputedReturns,
type InjectToObject,
type MergedComponentOptionsOverride,
type MethodOptions,
type OptionTypesKeys,
type OptionTypesType,
resolveMergedOptions,
shouldCacheAccess,
} from './componentOptions'
import type { EmitFn, EmitsOptions } from './componentEmits'
import type { SlotsType, UnwrapSlotsType } from './componentSlots'
import { markAttrsAccessed } from './componentRenderUtils'
import { currentRenderingInstance } from './componentRenderContext'
import { warn } from './warning'
import { installCompatInstanceProperties } from './compat/instance'
/**
* 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 {}
type IsDefaultMixinComponent<T> = T extends ComponentOptionsMixin
? ComponentOptionsMixin extends T
? true
: false
: false
type MixinToOptionTypes<T> =
T extends ComponentOptionsBase<
infer P,
infer B,
infer D,
infer C,
infer M,
infer Mixin,
infer Extends,
any,
any,
infer Defaults,
any,
any,
any
>
? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
IntersectionMixin<Mixin> &
IntersectionMixin<Extends>
: never
// ExtractMixin(map type) is used to resolve circularly references
type ExtractMixin<T> = {
Mixin: MixinToOptionTypes<T>
}[T extends ComponentOptionsMixin ? 'Mixin' : never]
export type IntersectionMixin<T> =
IsDefaultMixinComponent<T> extends true
? OptionTypesType
: UnionToIntersection<ExtractMixin<T>>
export type UnwrapMixinsType<
T,
Type extends OptionTypesKeys,
> = T extends OptionTypesType ? T[Type] : never
type EnsureNonVoid<T> = T extends void ? {} : T
export type ComponentPublicInstanceConstructor<
T extends ComponentPublicInstance<
Props,
RawBindings,
D,
C,
M
> = ComponentPublicInstance<any>,
Props = any,
RawBindings = any,
D = any,
C extends ComputedOptions = ComputedOptions,
M extends MethodOptions = MethodOptions,
> = {
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
new (...args: any[]): T
}
export type CreateComponentPublicInstance<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = {},
PublicProps = P,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
I extends ComponentInjectOptions = {},
S extends SlotsType = {},
PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
EnsureNonVoid<C>,
PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
EnsureNonVoid<M>,
PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
EnsureNonVoid<Defaults>,
> = ComponentPublicInstance<
PublicP,
PublicB,
PublicD,
PublicC,
PublicM,
E,
PublicProps,
PublicDefaults,
MakeDefaultsOptional,
ComponentOptionsBase<
P,
B,
D,
C,
M,
Mixin,
Extends,
E,
string,
Defaults,
{},
string,
S
>,
I,
S
>
// 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,
Defaults = {},
MakeDefaultsOptional extends boolean = false,
Options = ComponentOptionsBase<any, any, any, any, any, any, any, any, any>,
I extends ComponentInjectOptions = {},
S extends SlotsType = {},
> = {
$: ComponentInternalInstance
$data: D
$props: MakeDefaultsOptional extends true
? Partial<Defaults> & Omit<Prettify<P> & PublicProps, keyof Defaults>
: Prettify<P> & PublicProps
$attrs: Data
$refs: Data
$slots: UnwrapSlotsType<S>
$root: ComponentPublicInstance | null
$parent: ComponentPublicInstance | null
$emit: EmitFn<E>
$el: any
$options: Options & MergedComponentOptionsOverride
$forceUpdate: () => void
$nextTick: typeof nextTick
$watch<T extends string | ((...args: any) => any)>(
source: T,
cb: T extends (...args: any) => infer R
? (...args: [R, R]) => any
: (...args: any) => any,
options?: WatchOptions,
): WatchStopHandle
} & IfAny<P, P, Omit<P, keyof ShallowUnwrapRef<B>>> &
ShallowUnwrapRef<B> &
UnwrapNestedRefs<D> &
ExtractComputedReturns<C> &
M &
ComponentCustomProperties &
InjectToObject<I>
export type PublicPropertiesMap = Record<
string,
(i: ComponentInternalInstance) => any
>
/**
* #2437 In Vue 3, functional components do not have a public instance proxy but
* they exist in the internal parent chain. For code that relies on traversing
* public $parent chains, skip functional ones and go to the parent instead.
*/
const getPublicInstance = (
i: ComponentInternalInstance | null,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null => {
if (!i) return null
if (isStatefulComponent(i)) return getExposeProxy(i) || i.proxy
return getPublicInstance(i.parent)
}
export const publicPropertiesMap: PublicPropertiesMap =
// Move PURE marker to new line to workaround compiler discarding it
// due to type annotation
/*#__PURE__*/ extend(Object.create(null), {
$: 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 => getPublicInstance(i.parent),
$root: i => getPublicInstance(i.root),
$emit: i => i.emit,
$options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type),
$forceUpdate: i =>
i.f ||
(i.f = () => {
queueJob(i.update)
}),
$nextTick: i => i.n || (i.n = nextTick.bind(i.proxy!)),
$watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP),
} as PublicPropertiesMap)
if (__COMPAT__) {
installCompatInstanceProperties(publicPropertiesMap)
}
enum AccessTypes {
OTHER,
SETUP,
DATA,
PROPS,
CONTEXT,
}
export interface ComponentRenderContext {
[key: string]: any
_: ComponentInternalInstance
}
export const isReservedPrefix = (key: string) => key === '_' || key === '$'
const hasSetupBinding = (state: Data, key: string) =>
state !== EMPTY_OBJ && !state.__isScriptSetup && hasOwn(state, key)
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const { ctx, setupState, data, props, accessCache, type, appContext } =
instance
// for internal formatters to know that this is a Vue instance
if (__DEV__ && key === '__isVue') {
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.
let normalizedProps
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 (hasSetupBinding(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
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)
) {
accessCache![key] = AccessTypes.PROPS
return props![key]
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache![key] = AccessTypes.CONTEXT
return ctx[key]
} else if (!__FEATURE_OPTIONS_API__ || shouldCacheAccess) {
accessCache![key] = AccessTypes.OTHER
}
}
const publicGetter = publicPropertiesMap[key]
let cssModule, globalProperties
// public $xxx properties
if (publicGetter) {
if (key === '$attrs') {
track(instance, TrackOpTypes.GET, key)
__DEV__ && markAttrsAccessed()
} else if (__DEV__ && key === '$slots') {
track(instance, TrackOpTypes.GET, key)
}
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))
) {
if (__COMPAT__) {
const desc = Object.getOwnPropertyDescriptor(globalProperties, key)!
if (desc.get) {
return desc.get.call(instance.proxy)
} else {
const val = globalProperties[key]
return isFunction(val)
? Object.assign(val.bind(instance.proxy), val)
: val
}
} else {
return globalProperties[key]
}
} else if (
__DEV__ &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf('__v') !== 0)
) {
if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {
warn(
`Property ${JSON.stringify(
key,
)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`,
)
} else if (instance === currentRenderingInstance) {
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 (hasSetupBinding(setupState, key)) {
setupState[key] = value
return true
} else if (
__DEV__ &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)
) {
warn(`Cannot mutate <script setup> binding "${key}" from Options API.`)
return false
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value
return true
} else if (hasOwn(instance.props, key)) {
__DEV__ && warn(`Attempting to mutate prop "${key}". Props are readonly.`)
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.`,
)
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, appContext, propsOptions },
}: ComponentRenderContext,
key: string,
) {
let normalizedProps
return (
!!accessCache![key] ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
hasSetupBinding(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key)
)
},
defineProperty(
target: ComponentRenderContext,
key: string,
descriptor: PropertyDescriptor,
) {
if (descriptor.get != null) {
// invalidate key cache of a getter based property #5417
target._.accessCache![key] = 0
} else if (hasOwn(descriptor, 'value')) {
this.set!(target, key, descriptor.value, null)
}
return Reflect.defineProperty(target, key, descriptor)
},
}
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 = /*#__PURE__*/ extend(
{},
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] !== '_' && !isGloballyAllowed(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
},
},
)
// dev only
// 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 createDevRenderContext(instance: ComponentInternalInstance) {
const target: Record<string, any> = {}
// 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,
})
})
return target as ComponentRenderContext
}
// dev only
export function exposePropsOnRenderContext(
instance: ComponentInternalInstance,
) {
const {
ctx,
propsOptions: [propsOptions],
} = instance
if (propsOptions) {
Object.keys(propsOptions).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 => {
if (!setupState.__isScriptSetup) {
if (isReservedPrefix(key[0])) {
warn(
`setup() return property ${JSON.stringify(
key,
)} should not start with "$" or "_" ` +
`which are reserved prefixes for Vue internals.`,
)
return
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP,
})
}
})
}