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

416 lines
10 KiB
TypeScript
Raw Normal View History

2019-09-04 10:25:38 +08:00
import {
ComponentInstance,
Data,
2019-09-06 04:09:30 +08:00
currentInstance,
Component,
SetupContext
2019-09-04 10:25:38 +08:00
} from './component'
import {
isFunction,
extend,
isString,
isObject,
isArray,
2019-09-04 23:36:27 +08:00
EMPTY_OBJ,
capitalize,
camelize
2019-09-04 10:25:38 +08:00
} from '@vue/shared'
2019-09-06 04:09:30 +08:00
import { computed } from './apiReactivity'
import { watch, WatchOptions, CleanupRegistrator } from './apiWatch'
2019-09-04 10:25:38 +08:00
import { provide, inject } from './apiInject'
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onErrorCaptured,
onRenderTracked,
onBeforeUnmount,
onUnmounted
} from './apiLifecycle'
2019-09-05 06:16:11 +08:00
import { DebuggerEvent, reactive } from '@vue/reactivity'
2019-09-04 23:36:27 +08:00
import { warn } from './warning'
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { Directive } from './directives'
import { VNodeChild } from './vnode'
import { ComponentRenderProxy } from './componentProxy'
2019-09-06 23:25:11 +08:00
import { currentRenderingInstance } from './componentRenderUtils'
interface ComponentOptionsBase<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions
> extends LegacyOptions<Props, RawBindings, D, C, M> {
setup?: (
this: null,
props: Props,
ctx: SetupContext
) => RawBindings | (() => VNodeChild) | void
name?: string
template?: string
// 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>
}
export type ComponentOptionsWithoutProps<
Props = {},
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {}
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props?: undefined
} & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export type ComponentOptionsWithArrayProps<
PropNames extends string = string,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Props = { [key in PropNames]?: unknown }
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props: PropNames[]
} & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export type ComponentOptionsWithProps<
PropsOptions = ComponentPropsOptions,
RawBindings = {},
D = {},
C extends ComputedOptions = {},
M extends MethodOptions = {},
Props = ExtractPropTypes<PropsOptions>
> = ComponentOptionsBase<Props, RawBindings, D, C, M> & {
props: PropsOptions
} & ThisType<ComponentRenderProxy<Props, RawBindings, D, C, M>>
export type ComponentOptions =
| ComponentOptionsWithoutProps
| ComponentOptionsWithProps
| ComponentOptionsWithArrayProps
2019-09-04 10:25:38 +08:00
2019-09-04 23:36:27 +08:00
// TODO legacy component definition also supports constructors with .options
type LegacyComponent = ComponentOptions
2019-09-04 10:25:38 +08:00
2019-09-06 04:09:30 +08:00
export interface ComputedOptions {
[key: string]:
| Function
| {
get: Function
set: Function
}
}
export interface MethodOptions {
[key: string]: Function
}
export type ExtracComputedReturns<T extends any> = {
[key in keyof T]: T[key] extends { get: Function }
? ReturnType<T[key]['get']>
: ReturnType<T[key]>
}
type WatchHandler = (
val: any,
oldVal: any,
onCleanup: CleanupRegistrator
) => void
type ComponentWatchOptions = Record<
string,
string | WatchHandler | { handler: WatchHandler } & WatchOptions
>
type ComponentInjectOptions =
| string[]
| Record<
string | symbol,
string | symbol | { from: string | symbol; default?: any }
>
2019-09-04 10:25:38 +08:00
// TODO type inference for these options
2019-09-06 04:09:30 +08:00
export interface LegacyOptions<
Props,
RawBindings,
D,
C extends ComputedOptions,
M extends MethodOptions
2019-09-06 04:09:30 +08:00
> {
2019-09-04 10:25:38 +08:00
el?: any
// 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?: D | ((this: ComponentRenderProxy<Props>) => D)
computed?: C
methods?: M
2019-09-04 10:25:38 +08:00
// TODO watch array
watch?: ComponentWatchOptions
provide?: Data | Function
inject?: ComponentInjectOptions
2019-09-04 10:25:38 +08:00
// composition
mixins?: LegacyComponent[]
extends?: LegacyComponent
// lifecycle
beforeCreate?(): void
created?(): void
2019-09-04 10:25:38 +08:00
beforeMount?(): void
mounted?(): void
beforeUpdate?(): void
updated?(): void
activated?(): void
decativated?(): void
2019-09-05 22:07:43 +08:00
beforeUnmount?(): void
unmounted?(): void
2019-09-04 10:25:38 +08:00
renderTracked?(e: DebuggerEvent): void
renderTriggered?(e: DebuggerEvent): void
2019-09-05 01:50:57 +08:00
errorCaptured?(): boolean | void
2019-09-04 10:25:38 +08:00
}
2019-09-04 23:36:27 +08:00
export function applyOptions(
instance: ComponentInstance,
options: ComponentOptions,
asMixin: boolean = false
) {
const renderContext =
instance.renderContext === EMPTY_OBJ
? (instance.renderContext = reactive({}))
: instance.renderContext
2019-09-04 10:25:38 +08:00
const ctx = instance.renderProxy as any
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,
2019-09-04 23:36:27 +08:00
// assets
components,
directives,
// lifecycle
2019-09-04 10:25:38 +08:00
beforeMount,
mounted,
beforeUpdate,
updated,
// TODO activated
// TODO decativated
2019-09-05 22:07:43 +08:00
beforeUnmount,
unmounted,
2019-09-04 10:25:38 +08:00
renderTracked,
renderTriggered,
errorCaptured
2019-09-04 23:36:27 +08:00
} = options
2019-09-05 22:07:43 +08:00
const globalMixins = instance.appContext.mixins
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) {
callSyncHook('beforeCreate', options, ctx, globalMixins)
2019-09-06 08:59:45 +08:00
// global mixins are applied first
2019-09-05 22:07:43 +08:00
applyMixins(instance, globalMixins)
2019-09-04 23:36:27 +08:00
}
// extending a base component...
if (extendsOptions) {
applyOptions(instance, extendsOptions, true)
}
// local mixins
if (mixins) {
applyMixins(instance, mixins)
}
2019-09-04 10:25:38 +08:00
2019-09-04 23:36:27 +08:00
// state options
2019-09-04 10:25:38 +08:00
if (dataOptions) {
const data =
instance.data === EMPTY_OBJ
? (instance.data = reactive({}))
: instance.data
2019-09-04 10:25:38 +08:00
extend(data, isFunction(dataOptions) ? dataOptions.call(ctx) : dataOptions)
}
if (computedOptions) {
for (const key in computedOptions) {
2019-09-06 04:09:30 +08:00
const opt = (computedOptions as ComputedOptions)[key]
renderContext[key] = isFunction(opt)
2019-09-05 06:16:11 +08:00
? computed(opt.bind(ctx))
: computed({
get: opt.get.bind(ctx),
set: opt.set.bind(ctx)
})
2019-09-04 10:25:38 +08:00
}
}
if (methods) {
for (const key in methods) {
renderContext[key] = (methods as MethodOptions)[key].bind(ctx)
2019-09-04 10:25:38 +08:00
}
}
if (watchOptions) {
for (const key in watchOptions) {
const raw = watchOptions[key]
const getter = () => ctx[key]
if (isString(raw)) {
const handler = renderContext[raw]
2019-09-04 10:25:38 +08:00
if (isFunction(handler)) {
2019-09-05 06:16:11 +08:00
watch(getter, handler as any)
2019-09-04 10:25:38 +08:00
} else if (__DEV__) {
// TODO warn invalid watch handler path
}
} else if (isFunction(raw)) {
watch(getter, raw.bind(ctx))
} else if (isObject(raw)) {
2019-09-04 11:04:11 +08:00
// TODO 2.x compat
watch(getter, raw.handler.bind(ctx), raw)
2019-09-04 10:25:38 +08:00
} else if (__DEV__) {
// TODO warn invalid watch options
}
}
}
if (provideOptions) {
const provides = isFunction(provideOptions)
? provideOptions.call(ctx)
: provideOptions
for (const key in provides) {
provide(key, provides[key])
}
}
if (injectOptions) {
if (isArray(injectOptions)) {
for (let i = 0; i < injectOptions.length; i++) {
const key = injectOptions[i]
renderContext[key] = inject(key)
2019-09-04 10:25:38 +08:00
}
} else {
for (const key in injectOptions) {
const opt = injectOptions[key]
if (isObject(opt)) {
renderContext[key] = inject(opt.from, opt.default)
2019-09-04 10:25:38 +08:00
} else {
renderContext[key] = inject(opt)
2019-09-04 10:25:38 +08:00
}
}
}
}
2019-09-04 23:36:27 +08:00
// asset options
if (components) {
extend(instance.components, components)
}
if (directives) {
extend(instance.directives, directives)
}
// lifecycle options
2019-09-05 22:07:43 +08:00
if (!asMixin) {
callSyncHook('created', options, ctx, globalMixins)
2019-09-04 10:25:38 +08:00
}
if (beforeMount) {
onBeforeMount(beforeMount.bind(ctx))
}
if (mounted) {
onMounted(mounted.bind(ctx))
}
if (beforeUpdate) {
onBeforeUpdate(beforeUpdate.bind(ctx))
}
if (updated) {
onUpdated(updated.bind(ctx))
}
if (errorCaptured) {
onErrorCaptured(errorCaptured.bind(ctx))
}
if (renderTracked) {
onRenderTracked(renderTracked.bind(ctx))
}
if (renderTriggered) {
onRenderTracked(renderTriggered.bind(ctx))
}
2019-09-05 22:07:43 +08:00
if (beforeUnmount) {
onBeforeUnmount(beforeUnmount.bind(ctx))
2019-09-04 10:25:38 +08:00
}
2019-09-05 22:07:43 +08:00
if (unmounted) {
onUnmounted(unmounted.bind(ctx))
}
}
function callSyncHook(
name: 'beforeCreate' | 'created',
options: ComponentOptions,
ctx: any,
globalMixins: ComponentOptions[]
) {
callHookFromMixins(name, globalMixins, ctx)
const baseHook = options.extends && options.extends[name]
if (baseHook) {
baseHook.call(ctx)
}
const mixins = options.mixins
if (mixins) {
callHookFromMixins(name, mixins, ctx)
}
const selfHook = options[name]
if (selfHook) {
selfHook.call(ctx)
}
}
function callHookFromMixins(
name: 'beforeCreate' | 'created',
mixins: ComponentOptions[],
ctx: any
) {
for (let i = 0; i < mixins.length; i++) {
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-04 23:36:27 +08:00
function applyMixins(instance: ComponentInstance, mixins: ComponentOptions[]) {
for (let i = 0; i < mixins.length; i++) {
applyOptions(instance, mixins[i], true)
}
}
2019-09-06 23:25:11 +08:00
export function resolveComponent(name: string): Component | undefined {
return resolveAsset('components', name) as any
}
export function resolveDirective(name: string): Directive | undefined {
return resolveAsset('directives', name) as any
}
function resolveAsset(type: 'components' | 'directives', name: string) {
2019-09-04 23:36:27 +08:00
const instance = currentRenderingInstance || currentInstance
if (instance) {
let camelized
const registry = instance[type]
const res =
registry[name] ||
registry[(camelized = camelize(name))] ||
registry[capitalize(camelized)]
if (__DEV__ && !res) {
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}`)
}
return res
} else if (__DEV__) {
warn(
`resolve${capitalize(type.slice(0, -1))} ` +
`can only be used in render() or setup().`
)
}
2019-09-04 11:04:11 +08:00
}