vue3-yuanma/packages/runtime-core/src/componentOptions.ts

792 lines
20 KiB
TypeScript
Raw Normal View History

2019-09-04 10:25:38 +08:00
import {
2019-09-07 00:58:31 +08:00
ComponentInternalInstance,
2019-09-04 10:25:38 +08:00
Data,
2019-10-22 01:44:01 +08:00
SetupContext,
ComponentInternalOptions,
Component,
ConcreteComponent,
InternalRenderFunction
2019-09-04 10:25:38 +08:00
} from './component'
import {
isFunction,
extend,
isString,
isObject,
isArray,
EMPTY_OBJ,
NOOP,
2020-04-15 05:40:41 +08:00
hasOwn,
isPromise
2019-09-04 10:25:38 +08:00
} from '@vue/shared'
import { computed } from './apiComputed'
2019-12-31 00:30:12 +08:00
import { watch, WatchOptions, WatchCallback } from './apiWatch'
2019-09-04 10:25:38 +08:00
import { provide, inject } from './apiInject'
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onErrorCaptured,
onRenderTracked,
onBeforeUnmount,
onUnmounted,
2019-10-30 10:28:38 +08:00
onActivated,
onDeactivated,
2019-10-15 11:15:09 +08:00
onRenderTriggered,
DebuggerHook,
ErrorCapturedHook
2019-09-04 10:25:38 +08:00
} from './apiLifecycle'
2019-10-22 01:57:20 +08:00
import {
reactive,
ComputedGetter,
WritableComputedOptions,
toRaw
2019-10-22 01:57:20 +08:00
} from '@vue/reactivity'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps'
import { EmitsOptions } from './componentEmits'
import { Directive } from './directives'
import {
CreateComponentPublicInstance,
ComponentPublicInstance
} from './componentPublicInstance'
import { warn } from './warning'
import { VNodeChild } from './vnode'
/**
* Interface for declaring custom options.
*
* @example
* ```ts
* declare module '@vue/runtime-core' {
* interface ComponentCustomOptions {
* beforeRouteUpdate?(
* to: Route,
* from: Route,
* next: () => void
* ): void
* }
* }
* ```
*/
export interface ComponentCustomOptions {}
export type RenderFunction = () => VNodeChild
2019-10-22 23:26:48 +08:00
export interface ComponentOptionsBase<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin,
E extends EmitsOptions,
EE extends string = string
>
extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
ComponentInternalOptions,
ComponentCustomOptions {
setup?: (
2020-01-11 00:46:34 +08:00
this: void,
props: Props,
ctx: SetupContext<E>
2019-10-22 01:44:01 +08:00
) => RawBindings | RenderFunction | void
name?: string
2019-12-11 23:39:29 +08:00
template?: string | object // can be a direct DOM node
// Note: we are intentionally using the signature-less `Function` type here
// since any type with signature will cause the whole inference to fail when
// the return expression contains reference to `this`.
// Luckily `render()` doesn't need any arguments nor does it care about return
// type.
render?: Function
components?: Record<string, Component>
directives?: Record<string, Directive>
inheritAttrs?: boolean
emits?: (E | EE[]) & ThisType<void>
2020-09-02 10:52:46 +08:00
serverPrefetch?(): Promise<any>
// Internal ------------------------------------------------------------------
/**
* SSR only. This is produced by compiler-ssr and attached in compiler-sfc
* not user facing, so the typing is lax and for test only.
*
* @internal
*/
ssrRender?: (
ctx: any,
push: (item: any) => void,
parentInstance: ComponentInternalInstance,
2020-07-11 10:12:25 +08:00
attrs: Data | undefined,
// for compiler-optimized bindings
$props: ComponentInternalInstance['props'],
$setup: ComponentInternalInstance['setupState'],
$data: ComponentInternalInstance['data'],
$options: ComponentInternalInstance['ctx']
) => void
/**
* marker for AsyncComponentWrapper
* @internal
*/
__asyncLoader?: () => Promise<ConcreteComponent>
/**
* cache for merged $options
* @internal
*/
__merged?: ComponentOptions
// Type differentiators ------------------------------------------------------
// Note these are internal but need to be exposed in d.ts for type inference
// to work!
2019-11-28 17:01:53 +08:00
// type-only differentiator to separate OptionWithoutProps from a constructor
// type returned by defineComponent() or FunctionalComponent
call?: (this: unknown, ...args: unknown[]) => never
// type-only differentiators for built-in Vnode types
__isFragment?: never
__isTeleport?: never
__isSuspense?: never
}
export type ComponentOptionsWithoutProps<
Props = {},
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string
> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
props?: undefined
} & ThisType<
CreateComponentPublicInstance<
{},
RawBindings,
D,
C,
M,
Mixin,
Extends,
E,
Readonly<Props>
>
>
export type ComponentOptionsWithArrayProps<
PropNames extends string = string,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
2019-11-10 07:40:25 +08:00
Props = Readonly<{ [key in PropNames]?: any }>
> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
props: PropNames[]
} & ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E
>
>
2019-10-08 21:26:09 +08:00
export type ComponentOptionsWithObjectProps<
PropsOptions = ComponentObjectPropsOptions,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
E extends EmitsOptions = EmitsOptions,
EE extends string = string,
2019-11-10 07:40:25 +08:00
Props = Readonly<ExtractPropTypes<PropsOptions>>
> = ComponentOptionsBase<Props, RawBindings, D, C, M, Mixin, Extends, E, EE> & {
props: PropsOptions & ThisType<void>
} & ThisType<
CreateComponentPublicInstance<
Props,
RawBindings,
D,
C,
M,
Mixin,
Extends,
E
>
>
export type ComponentOptions =
| ComponentOptionsWithoutProps<any, any, any, any, any>
| ComponentOptionsWithObjectProps<any, any, any, any, any>
| ComponentOptionsWithArrayProps<any, any, any, any, any>
2019-09-04 10:25:38 +08:00
export type ComponentOptionsMixin = ComponentOptionsBase<
any,
any,
any,
any,
any,
any,
any,
any,
any
>
2019-10-22 01:57:20 +08:00
export type ComputedOptions = Record<
string,
ComputedGetter<any> | WritableComputedOptions<any>
>
2019-09-06 04:09:30 +08:00
export interface MethodOptions {
[key: string]: Function
}
export type ExtractComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: (...args: any[]) => infer TReturn }
? TReturn
: T[key] extends (...args: any[]) => infer TReturn ? TReturn : never
2019-09-06 04:09:30 +08:00
}
type WatchOptionItem =
| string
2019-12-31 00:30:12 +08:00
| WatchCallback
| { handler: WatchCallback | string } & WatchOptions
type ComponentWatchOptionItem = WatchOptionItem | WatchOptionItem[]
type ComponentWatchOptions = Record<string, ComponentWatchOptionItem>
type ComponentInjectOptions =
| string[]
| Record<
string | symbol,
2019-10-22 23:26:48 +08:00
string | symbol | { from: string | symbol; default?: unknown }
>
interface LegacyOptions<
2019-09-06 04:09:30 +08:00
Props,
D,
C extends ComputedOptions,
M extends MethodOptions,
Mixin extends ComponentOptionsMixin,
Extends extends ComponentOptionsMixin
2019-09-06 04:09:30 +08:00
> {
// allow any custom options
[key: string]: any
2019-09-04 10:25:38 +08:00
// state
// Limitation: we cannot expose RawBindings on the `this` context for data
// since that leads to some sort of circular inference and breaks ThisType
// for the entire component.
data?: (
this: CreateComponentPublicInstance<Props>,
vm: CreateComponentPublicInstance<Props>
) => D
computed?: C
methods?: M
watch?: ComponentWatchOptions
provide?: Data | Function
inject?: ComponentInjectOptions
2019-09-04 10:25:38 +08:00
// composition
mixins?: Mixin[]
extends?: Extends
2019-09-04 10:25:38 +08:00
// lifecycle
beforeCreate?(): void
created?(): void
2019-09-04 10:25:38 +08:00
beforeMount?(): void
mounted?(): void
beforeUpdate?(): void
updated?(): void
activated?(): void
2019-10-05 22:48:54 +08:00
deactivated?(): void
2019-09-05 22:07:43 +08:00
beforeUnmount?(): void
unmounted?(): void
2019-10-15 11:15:09 +08:00
renderTracked?: DebuggerHook
renderTriggered?: DebuggerHook
errorCaptured?: ErrorCapturedHook
// runtime compile only
delimiters?: [string, string]
2019-09-04 10:25:38 +08:00
}
export type OptionTypesKeys = 'P' | 'B' | 'D' | 'C' | 'M'
export type OptionTypesType<
P = {},
B = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {}
> = {
P: P
B: B
D: D
C: C
M: M
}
const enum OptionTypes {
PROPS = 'Props',
DATA = 'Data',
COMPUTED = 'Computed',
METHODS = 'Methods',
INJECT = 'Inject'
}
function createDuplicateChecker() {
const cache = Object.create(null)
return (type: OptionTypes, key: string) => {
if (cache[key]) {
warn(`${type} property "${key}" is already defined in ${cache[key]}.`)
} else {
cache[key] = type
}
}
}
type DataFn = (vm: ComponentPublicInstance) => any
export let isInBeforeCreate = false
2019-09-04 23:36:27 +08:00
export function applyOptions(
2019-09-07 00:58:31 +08:00
instance: ComponentInternalInstance,
2019-09-04 23:36:27 +08:00
options: ComponentOptions,
deferredData: DataFn[] = [],
deferredWatch: ComponentWatchOptions[] = [],
2019-09-04 23:36:27 +08:00
asMixin: boolean = false
) {
2019-09-04 10:25:38 +08:00
const {
2019-09-04 23:36:27 +08:00
// composition
mixins,
extends: extendsOptions,
// state
2019-09-04 10:25:38 +08:00
data: dataOptions,
computed: computedOptions,
methods,
watch: watchOptions,
provide: provideOptions,
inject: injectOptions,
// assets
components,
directives,
2019-09-04 23:36:27 +08:00
// lifecycle
2019-09-04 10:25:38 +08:00
beforeMount,
mounted,
beforeUpdate,
updated,
2019-10-30 10:28:38 +08:00
activated,
deactivated,
2019-09-05 22:07:43 +08:00
beforeUnmount,
unmounted,
render,
2019-09-04 10:25:38 +08:00
renderTracked,
renderTriggered,
errorCaptured
2019-09-04 23:36:27 +08:00
} = options
const publicThis = instance.proxy!
const ctx = instance.ctx
2019-09-05 22:07:43 +08:00
const globalMixins = instance.appContext.mixins
if (asMixin && render && instance.render === NOOP) {
instance.render = render as InternalRenderFunction
}
2019-09-06 08:59:45 +08:00
// applyOptions is called non-as-mixin once per instance
2019-09-05 22:07:43 +08:00
if (!asMixin) {
isInBeforeCreate = true
callSyncHook('beforeCreate', options, publicThis, globalMixins)
isInBeforeCreate = false
2019-09-06 08:59:45 +08:00
// global mixins are applied first
applyMixins(instance, globalMixins, deferredData, deferredWatch)
2019-09-04 23:36:27 +08:00
}
2019-09-04 23:36:27 +08:00
// extending a base component...
if (extendsOptions) {
applyOptions(instance, extendsOptions, deferredData, deferredWatch, true)
2019-09-04 23:36:27 +08:00
}
// local mixins
if (mixins) {
applyMixins(instance, mixins, deferredData, deferredWatch)
2019-09-04 23:36:27 +08:00
}
2019-09-04 10:25:38 +08:00
const checkDuplicateProperties = __DEV__ ? createDuplicateChecker() : null
if (__DEV__) {
const [propsOptions] = instance.propsOptions
if (propsOptions) {
for (const key in propsOptions) {
checkDuplicateProperties!(OptionTypes.PROPS, key)
}
}
}
// options initialization order (to be consistent with Vue 2):
// - props (already done outside of this function)
// - inject
// - methods
// - data (deferred since it relies on `this` access)
// - computed
// - watch (deferred since it relies on `this` access)
if (injectOptions) {
if (isArray(injectOptions)) {
for (let i = 0; i < injectOptions.length; i++) {
const key = injectOptions[i]
ctx[key] = inject(key)
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
} else {
for (const key in injectOptions) {
const opt = injectOptions[key]
if (isObject(opt)) {
ctx[key] = inject(opt.from, opt.default)
} else {
ctx[key] = inject(opt)
}
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.INJECT, key)
}
}
}
}
if (methods) {
for (const key in methods) {
const methodHandler = (methods as MethodOptions)[key]
if (isFunction(methodHandler)) {
ctx[key] = methodHandler.bind(publicThis)
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.METHODS, key)
}
} else if (__DEV__) {
warn(
`Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
`Did you reference the function correctly?`
)
}
}
}
if (!asMixin) {
if (deferredData.length) {
deferredData.forEach(dataFn => resolveData(instance, dataFn, publicThis))
}
if (dataOptions) {
resolveData(instance, dataOptions, publicThis)
}
if (__DEV__) {
const rawData = toRaw(instance.data)
for (const key in rawData) {
checkDuplicateProperties!(OptionTypes.DATA, key)
// expose data on ctx during dev
if (key[0] !== '$' && key[0] !== '_') {
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => rawData[key],
set: NOOP
})
}
}
}
} else if (dataOptions) {
deferredData.push(dataOptions as DataFn)
2019-09-04 10:25:38 +08:00
}
2019-09-04 10:25:38 +08:00
if (computedOptions) {
for (const key in computedOptions) {
2019-09-06 04:09:30 +08:00
const opt = (computedOptions as ComputedOptions)[key]
const get = isFunction(opt)
? opt.bind(publicThis, publicThis)
: isFunction(opt.get)
? opt.get.bind(publicThis, publicThis)
: NOOP
if (__DEV__ && get === NOOP) {
warn(`Computed property "${key}" has no getter.`)
}
const set =
!isFunction(opt) && isFunction(opt.set)
? opt.set.bind(publicThis)
: __DEV__
? () => {
warn(
`Write operation failed: computed property "${key}" is readonly.`
)
}
: NOOP
const c = computed({
get,
set
})
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => c.value,
set: v => (c.value = v)
})
if (__DEV__) {
checkDuplicateProperties!(OptionTypes.COMPUTED, key)
}
2019-09-04 10:25:38 +08:00
}
}
2019-09-04 10:25:38 +08:00
if (watchOptions) {
deferredWatch.push(watchOptions)
}
if (!asMixin && deferredWatch.length) {
deferredWatch.forEach(watchOptions => {
for (const key in watchOptions) {
createWatcher(watchOptions[key], ctx, publicThis, key)
}
})
2019-09-04 10:25:38 +08:00
}
2019-09-04 10:25:38 +08:00
if (provideOptions) {
const provides = isFunction(provideOptions)
? provideOptions.call(publicThis)
2019-09-04 10:25:38 +08:00
: provideOptions
for (const key in provides) {
provide(key, provides[key])
}
}
// asset options.
// To reduce memory usage, only components with mixins or extends will have
// resolved asset registry attached to instance.
if (asMixin) {
if (components) {
extend(
instance.components ||
(instance.components = extend(
{},
(instance.type as ComponentOptions).components
) as Record<string, ConcreteComponent>),
components
)
}
if (directives) {
extend(
instance.directives ||
(instance.directives = extend(
{},
(instance.type as ComponentOptions).directives
)),
directives
)
}
}
2019-09-04 23:36:27 +08:00
// lifecycle options
2019-09-05 22:07:43 +08:00
if (!asMixin) {
callSyncHook('created', options, publicThis, globalMixins)
2019-09-04 10:25:38 +08:00
}
if (beforeMount) {
onBeforeMount(beforeMount.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
if (mounted) {
onMounted(mounted.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
if (beforeUpdate) {
onBeforeUpdate(beforeUpdate.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
if (updated) {
onUpdated(updated.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
2019-10-30 10:28:38 +08:00
if (activated) {
onActivated(activated.bind(publicThis))
2019-10-30 10:28:38 +08:00
}
if (deactivated) {
onDeactivated(deactivated.bind(publicThis))
2019-10-30 10:28:38 +08:00
}
2019-09-04 10:25:38 +08:00
if (errorCaptured) {
onErrorCaptured(errorCaptured.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
if (renderTracked) {
onRenderTracked(renderTracked.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
if (renderTriggered) {
onRenderTriggered(renderTriggered.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
2019-09-05 22:07:43 +08:00
if (beforeUnmount) {
onBeforeUnmount(beforeUnmount.bind(publicThis))
2019-09-04 10:25:38 +08:00
}
2019-09-05 22:07:43 +08:00
if (unmounted) {
onUnmounted(unmounted.bind(publicThis))
2019-09-05 22:07:43 +08:00
}
}
function callSyncHook(
name: 'beforeCreate' | 'created',
options: ComponentOptions,
2019-10-22 11:37:03 +08:00
ctx: ComponentPublicInstance,
2019-09-05 22:07:43 +08:00
globalMixins: ComponentOptions[]
) {
callHookFromMixins(name, globalMixins, ctx)
const { extends: base, mixins } = options
if (base) {
callHookFromExtends(name, base, ctx)
2019-09-05 22:07:43 +08:00
}
if (mixins) {
callHookFromMixins(name, mixins, ctx)
}
const selfHook = options[name]
if (selfHook) {
selfHook.call(ctx)
}
}
function callHookFromExtends(
name: 'beforeCreate' | 'created',
base: ComponentOptions,
ctx: ComponentPublicInstance
) {
if (base.extends) {
callHookFromExtends(name, base.extends, ctx)
}
const baseHook = base[name]
if (baseHook) {
baseHook.call(ctx)
}
}
2019-09-05 22:07:43 +08:00
function callHookFromMixins(
name: 'beforeCreate' | 'created',
mixins: ComponentOptions[],
2019-10-22 11:37:03 +08:00
ctx: ComponentPublicInstance
2019-09-05 22:07:43 +08:00
) {
for (let i = 0; i < mixins.length; i++) {
const chainedMixins = mixins[i].mixins
if (chainedMixins) {
callHookFromMixins(name, chainedMixins, ctx)
}
2019-09-05 22:07:43 +08:00
const fn = mixins[i][name]
if (fn) {
fn.call(ctx)
}
2019-09-04 10:25:38 +08:00
}
}
2019-09-04 11:04:11 +08:00
2019-09-07 00:58:31 +08:00
function applyMixins(
instance: ComponentInternalInstance,
mixins: ComponentOptions[],
deferredData: DataFn[],
deferredWatch: ComponentWatchOptions[]
2019-09-07 00:58:31 +08:00
) {
2019-09-04 23:36:27 +08:00
for (let i = 0; i < mixins.length; i++) {
applyOptions(instance, mixins[i], deferredData, deferredWatch, true)
}
}
function resolveData(
instance: ComponentInternalInstance,
dataFn: DataFn,
publicThis: ComponentPublicInstance
) {
if (__DEV__ && !isFunction(dataFn)) {
warn(
`The data option must be a function. ` +
`Plain object usage is no longer supported.`
)
}
const data = dataFn.call(publicThis, publicThis)
if (__DEV__ && isPromise(data)) {
warn(
`data() returned a Promise - note data() cannot be async; If you ` +
`intend to perform data fetching before component renders, use ` +
`async setup() + <Suspense>.`
)
}
if (!isObject(data)) {
__DEV__ && warn(`data() should return an object.`)
} else if (instance.data === EMPTY_OBJ) {
instance.data = reactive(data)
} else {
// existing data: this is a mixin or extends.
extend(instance.data, data)
2019-09-04 23:36:27 +08:00
}
}
function createWatcher(
raw: ComponentWatchOptionItem,
ctx: Data,
publicThis: ComponentPublicInstance,
key: string
) {
const getter = () => (publicThis as any)[key]
if (isString(raw)) {
const handler = ctx[raw]
if (isFunction(handler)) {
2019-12-31 00:30:12 +08:00
watch(getter, handler as WatchCallback)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw}"`, handler)
}
} else if (isFunction(raw)) {
watch(getter, raw.bind(publicThis))
} else if (isObject(raw)) {
if (isArray(raw)) {
raw.forEach(r => createWatcher(r, ctx, publicThis, key))
} else {
const handler = isFunction(raw.handler)
? raw.handler.bind(publicThis)
: (ctx[raw.handler] as WatchCallback)
if (isFunction(handler)) {
watch(getter, handler, raw)
} else if (__DEV__) {
warn(`Invalid watch handler specified by key "${raw.handler}"`, handler)
}
}
} else if (__DEV__) {
warn(`Invalid watch option: "${key}"`)
}
}
export function resolveMergedOptions(
instance: ComponentInternalInstance
): ComponentOptions {
const raw = instance.type as ComponentOptions
const { __merged, mixins, extends: extendsOptions } = raw
if (__merged) return __merged
const globalMixins = instance.appContext.mixins
if (!globalMixins.length && !mixins && !extendsOptions) return raw
const options = {}
globalMixins.forEach(m => mergeOptions(options, m, instance))
mergeOptions(options, raw, instance)
return (raw.__merged = options)
}
function mergeOptions(to: any, from: any, instance: ComponentInternalInstance) {
const strats = instance.appContext.config.optionMergeStrategies
const { mixins, extends: extendsOptions } = from
extendsOptions && mergeOptions(to, extendsOptions, instance)
mixins &&
mixins.forEach((m: ComponentOptionsMixin) => mergeOptions(to, m, instance))
for (const key in from) {
if (strats && hasOwn(strats, key)) {
to[key] = strats[key](to[key], from[key], instance.proxy, key)
} else {
to[key] = from[key]
}
}
}