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

505 lines
15 KiB
TypeScript
Raw Normal View History

import { VNode, normalizeVNode, VNodeChild, createVNode, Empty } from './vnode'
2019-08-23 21:38:32 +08:00
import { ReactiveEffect, UnwrapRef, reactive, readonly } from '@vue/reactivity'
2019-09-04 10:25:38 +08:00
import {
EMPTY_OBJ,
isFunction,
capitalize,
NOOP,
isArray,
isObject
} from '@vue/shared'
2019-05-29 10:43:27 +08:00
import { RenderProxyHandlers } from './componentProxy'
2019-05-31 18:07:43 +08:00
import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'
import { Slots } from './componentSlots'
2019-08-22 23:12:37 +08:00
import { PatchFlags } from './patchFlags'
import { ShapeFlags } from './shapeFlags'
import { warn } from './warning'
import {
2019-08-31 03:15:23 +08:00
ErrorTypes,
handleError,
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
2019-09-04 23:36:27 +08:00
import { AppContext, createAppContext } from './apiApp'
2019-09-04 06:11:04 +08:00
import { Directive } from './directives'
2019-09-04 23:36:27 +08:00
import { applyOptions, LegacyOptions, resolveAsset } from './apiOptions'
2019-05-28 13:27:31 +08:00
2019-08-13 23:18:23 +08:00
export type Data = { [key: string]: unknown }
2019-05-29 10:43:27 +08:00
2019-06-13 10:25:24 +08:00
// public properties exposed on the proxy, which is used as the render context
// in templates (as `this` in the render option)
export type ComponentRenderProxy<P = {}, S = {}, PublicProps = P> = {
2019-06-19 17:08:42 +08:00
$data: S
$props: PublicProps
2019-05-28 18:06:00 +08:00
$attrs: Data
$refs: Data
$slots: Data
2019-05-29 13:43:46 +08:00
$root: ComponentInstance | null
$parent: ComponentInstance | null
2019-08-13 23:18:23 +08:00
$emit: (event: string, ...args: unknown[]) => void
2019-05-30 13:35:50 +08:00
} & P &
S
2019-05-28 18:06:00 +08:00
2019-06-19 16:43:34 +08:00
type RenderFunction<Props = {}, RawBindings = {}> = <
Bindings extends UnwrapRef<RawBindings>
>(
this: ComponentRenderProxy<Props, Bindings>
) => VNodeChild
2019-09-04 10:25:38 +08:00
interface ComponentOptionsBase<Props, RawBindings> extends LegacyOptions {
2019-09-04 06:11:04 +08:00
setup?: (
props: Props,
ctx: SetupContext
) => RawBindings | (() => VNodeChild) | void
2019-09-04 10:25:38 +08:00
name?: string
template?: string
2019-06-19 16:43:34 +08:00
render?: RenderFunction<Props, RawBindings>
2019-09-04 06:11:04 +08:00
components?: Record<string, Component>
directives?: Record<string, Directive>
}
interface ComponentOptionsWithoutProps<Props = {}, RawBindings = {}>
extends ComponentOptionsBase<Props, RawBindings> {
props?: undefined
}
2019-06-13 10:25:24 +08:00
interface ComponentOptionsWithArrayProps<
PropNames extends string = string,
2019-09-04 06:11:04 +08:00
RawBindings = {},
2019-08-13 23:18:23 +08:00
Props = { [key in PropNames]?: unknown }
2019-09-04 06:11:04 +08:00
> extends ComponentOptionsBase<Props, RawBindings> {
2019-06-13 10:25:24 +08:00
props: PropNames[]
}
2019-06-13 10:25:24 +08:00
interface ComponentOptionsWithProps<
PropsOptions = ComponentPropsOptions,
2019-09-04 06:11:04 +08:00
RawBindings = {},
2019-06-13 10:25:24 +08:00
Props = ExtractPropTypes<PropsOptions>
2019-09-04 06:11:04 +08:00
> extends ComponentOptionsBase<Props, RawBindings> {
2019-06-13 10:25:24 +08:00
props: PropsOptions
2019-05-28 17:19:47 +08:00
}
2019-05-28 13:27:31 +08:00
2019-06-13 10:25:24 +08:00
export type ComponentOptions =
2019-06-12 16:22:52 +08:00
| ComponentOptionsWithProps
| ComponentOptionsWithoutProps
| ComponentOptionsWithArrayProps
2019-06-19 16:43:34 +08:00
export interface FunctionalComponent<P = {}> {
(props: P, ctx: SetupContext): VNodeChild
2019-05-28 18:06:00 +08:00
props?: ComponentPropsOptions<P>
displayName?: string
}
2019-09-03 04:09:34 +08:00
export type Component = ComponentOptions | FunctionalComponent
2019-05-28 19:36:15 +08:00
type LifecycleHook = Function[] | null
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec'
2019-05-28 19:36:15 +08:00
}
2019-06-19 16:43:34 +08:00
interface SetupContext {
attrs: Data
slots: Slots
2019-08-13 23:18:23 +08:00
emit: ((event: string, ...args: unknown[]) => void)
2019-06-19 16:43:34 +08:00
}
2019-09-04 06:24:32 +08:00
export type ComponentInstance<P = Data, S = Data> = {
2019-05-28 18:06:00 +08:00
type: FunctionalComponent | ComponentOptions
2019-06-03 09:43:28 +08:00
parent: ComponentInstance | null
2019-09-03 04:09:34 +08:00
appContext: AppContext
2019-06-03 09:43:28 +08:00
root: ComponentInstance
2019-05-29 10:43:27 +08:00
vnode: VNode
2019-05-28 17:19:47 +08:00
next: VNode | null
2019-05-29 10:43:27 +08:00
subTree: VNode
2019-05-28 17:19:47 +08:00
update: ReactiveEffect
2019-06-19 16:43:34 +08:00
render: RenderFunction<P, S> | null
2019-06-19 17:31:49 +08:00
effects: ReactiveEffect[] | null
2019-06-19 22:48:22 +08:00
provides: Data
2019-06-19 16:43:34 +08:00
2019-09-04 23:36:27 +08:00
components: Record<string, Component>
directives: Record<string, Directive>
2019-05-29 10:43:27 +08:00
// the rest are only for stateful components
2019-06-19 17:08:42 +08:00
data: S
2019-05-29 11:36:16 +08:00
props: P
2019-06-19 16:43:34 +08:00
renderProxy: ComponentRenderProxy | null
propsProxy: P | null
setupContext: SetupContext | null
refs: Data
2019-08-21 21:50:20 +08:00
// user namespace
user: { [key: string]: any }
// lifecycle
[LifecycleHooks.BEFORE_CREATE]: LifecycleHook
[LifecycleHooks.CREATED]: LifecycleHook
[LifecycleHooks.BEFORE_MOUNT]: LifecycleHook
[LifecycleHooks.MOUNTED]: LifecycleHook
[LifecycleHooks.BEFORE_UPDATE]: LifecycleHook
[LifecycleHooks.UPDATED]: LifecycleHook
[LifecycleHooks.BEFORE_UNMOUNT]: LifecycleHook
[LifecycleHooks.UNMOUNTED]: LifecycleHook
[LifecycleHooks.RENDER_TRACKED]: LifecycleHook
[LifecycleHooks.RENDER_TRIGGERED]: LifecycleHook
[LifecycleHooks.ACTIVATED]: LifecycleHook
[LifecycleHooks.DEACTIVATED]: LifecycleHook
[LifecycleHooks.ERROR_CAPTURED]: LifecycleHook
} & SetupContext
2019-05-28 19:36:15 +08:00
2019-06-13 10:25:24 +08:00
// createComponent
// overload 1: direct setup function
// (uses user defined props interface)
export function createComponent<Props>(
2019-08-22 05:10:37 +08:00
setup: (props: Props, ctx: SetupContext) => object | (() => VNodeChild)
): (props: Props) => any
2019-06-13 10:25:24 +08:00
// overload 2: object format with no props
// (uses user defined props interface)
// return type is for Vetur and TSX support
export function createComponent<Props, RawBindings>(
options: ComponentOptionsWithoutProps<Props, RawBindings>
): {
new (): ComponentRenderProxy<Props, UnwrapRef<RawBindings>>
2019-06-13 10:25:24 +08:00
}
// overload 3: object format with array props declaration
2019-08-13 23:18:23 +08:00
// props inferred as { [key in PropNames]?: unknown }
2019-06-13 10:25:24 +08:00
// return type is for Vetur and TSX support
export function createComponent<PropNames extends string, RawBindings>(
options: ComponentOptionsWithArrayProps<PropNames, RawBindings>
): {
new (): ComponentRenderProxy<
2019-08-13 23:18:23 +08:00
{ [key in PropNames]?: unknown },
UnwrapRef<RawBindings>
>
}
2019-06-13 10:25:24 +08:00
// overload 4: object format with object props declaration
// see `ExtractPropTypes` in ./componentProps.ts
export function createComponent<PropsOptions, RawBindings>(
options: ComponentOptionsWithProps<PropsOptions, RawBindings>
2019-05-29 10:43:27 +08:00
): {
// for Vetur and TSX support
new (): ComponentRenderProxy<
ExtractPropTypes<PropsOptions>,
UnwrapRef<RawBindings>,
ExtractPropTypes<PropsOptions, false>
>
}
2019-06-13 10:25:24 +08:00
// implementation, close to no-op
export function createComponent(options: any) {
return isFunction(options) ? { setup: options } : (options as any)
2019-05-29 10:43:27 +08:00
}
2019-09-03 04:09:34 +08:00
const emptyAppContext = createAppContext()
2019-06-03 09:43:28 +08:00
export function createComponentInstance(
2019-08-29 00:13:36 +08:00
vnode: VNode,
2019-06-03 09:43:28 +08:00
parent: ComponentInstance | null
): ComponentInstance {
2019-09-04 06:11:04 +08:00
// inherit parent app context - or - if root, adopt from root vnode
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
2019-06-03 09:43:28 +08:00
const instance = {
2019-08-29 00:13:36 +08:00
vnode,
2019-06-03 09:43:28 +08:00
parent,
2019-09-04 06:11:04 +08:00
appContext,
2019-09-04 23:36:27 +08:00
type: vnode.type as Component,
2019-06-03 09:43:28 +08:00
root: null as any, // set later so it can point to itself
2019-05-28 19:36:15 +08:00
next: null,
2019-05-29 10:43:27 +08:00
subTree: null as any,
2019-05-28 19:36:15 +08:00
update: null as any,
render: null,
2019-05-30 23:16:15 +08:00
renderProxy: null,
propsProxy: null,
2019-06-19 16:43:34 +08:00
setupContext: null,
effects: null,
2019-09-04 06:11:04 +08:00
provides: parent ? parent.provides : Object.create(appContext.provides),
2019-05-28 19:36:15 +08:00
// setup context properties
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
2019-09-04 23:36:27 +08:00
// per-instance asset storage (mutable during options resolution)
components: Object.create(appContext.components),
directives: Object.create(appContext.directives),
// user namespace for storing whatever the user assigns to `this`
user: {},
// lifecycle hooks
// not using enums here because it results in computed properties
bc: null,
c: null,
2019-05-28 19:36:15 +08:00
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
2019-08-21 21:50:20 +08:00
2019-08-13 23:18:23 +08:00
emit: (event: string, ...args: unknown[]) => {
2019-06-19 16:43:34 +08:00
const props = instance.vnode.props || EMPTY_OBJ
const handler = props[`on${event}`] || props[`on${capitalize(event)}`]
if (handler) {
if (isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
callWithAsyncErrorHandling(
handler[i],
instance,
2019-08-31 03:15:23 +08:00
ErrorTypes.COMPONENT_EVENT_HANDLER,
args
)
}
} else {
callWithAsyncErrorHandling(
handler,
instance,
2019-08-31 03:15:23 +08:00
ErrorTypes.COMPONENT_EVENT_HANDLER,
args
)
}
2019-06-19 16:43:34 +08:00
}
}
2019-05-28 19:36:15 +08:00
}
2019-06-03 09:43:28 +08:00
instance.root = parent ? parent.root : instance
return instance
2019-05-28 19:36:15 +08:00
}
export let currentInstance: ComponentInstance | null = null
2019-06-20 15:25:10 +08:00
export const getCurrentInstance: () => ComponentInstance | null = () =>
currentInstance
export const setCurrentInstance = (instance: ComponentInstance | null) => {
currentInstance = instance
}
2019-05-29 11:36:16 +08:00
export function setupStatefulComponent(instance: ComponentInstance) {
2019-09-04 10:25:38 +08:00
currentInstance = instance
2019-05-29 10:43:27 +08:00
const Component = instance.type as ComponentOptions
2019-05-28 19:36:15 +08:00
// 1. create render proxy
2019-06-19 17:08:42 +08:00
instance.renderProxy = new Proxy(instance, RenderProxyHandlers) as any
// 2. create props proxy
// the propsProxy is a reactive AND readonly proxy to the actual props.
// it will be updated in resolveProps() on updates before render
const propsProxy = (instance.propsProxy = readonly(instance.props))
// 3. call setup()
2019-05-30 23:16:15 +08:00
const { setup } = Component
if (setup) {
2019-06-19 16:43:34 +08:00
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
const setupResult = callWithErrorHandling(
setup,
instance,
2019-08-31 03:15:23 +08:00
ErrorTypes.SETUP_FUNCTION,
[propsProxy, setupContext]
)
2019-06-19 22:48:22 +08:00
if (isFunction(setupResult)) {
2019-06-13 10:25:24 +08:00
// setup returned an inline render function
instance.render = setupResult
} else {
2019-08-31 04:20:32 +08:00
if (__DEV__) {
if (!Component.render) {
warn(
`Component is missing render function. Either provide a template or ` +
`return a render function from setup().`
)
}
if (
setupResult &&
typeof setupResult.then === 'function' &&
typeof setupResult.catch === 'function'
) {
warn(`setup() returned a Promise. setup() cannot be async.`)
}
}
2019-06-13 10:25:24 +08:00
// setup returned bindings.
// assuming a render function compiled from template is present.
2019-09-04 10:25:38 +08:00
if (isObject(setupResult)) {
2019-09-05 06:16:11 +08:00
instance.data = reactive(setupResult)
2019-09-04 10:25:38 +08:00
} else if (__DEV__ && setupResult !== undefined) {
warn(
`setup() should return an object. Received: ${
setupResult === null ? 'null' : typeof setupResult
}`
)
}
instance.render = (Component.render || NOOP) as RenderFunction
}
2019-08-22 05:05:14 +08:00
} else {
if (__DEV__ && !Component.render) {
2019-09-04 10:25:38 +08:00
warn(
`Component is missing render function. Either provide a template or ` +
`return a render function from setup().`
)
2019-08-22 05:05:14 +08:00
}
instance.render = Component.render as RenderFunction
2019-05-28 19:36:15 +08:00
}
2019-09-04 10:25:38 +08:00
// support for 2.x options
if (__FEATURE_OPTIONS__) {
2019-09-04 23:36:27 +08:00
applyOptions(instance, Component)
2019-09-04 10:25:38 +08:00
}
2019-09-05 06:16:11 +08:00
if (instance.data === EMPTY_OBJ) {
instance.data = reactive({})
}
2019-09-04 10:25:38 +08:00
currentInstance = null
2019-05-28 19:36:15 +08:00
}
2019-05-28 17:19:47 +08:00
2019-08-23 10:07:51 +08:00
// used to identify a setup context proxy
export const SetupProxySymbol = Symbol()
2019-06-19 16:43:34 +08:00
const SetupProxyHandlers: { [key: string]: ProxyHandler<any> } = {}
;['attrs', 'slots', 'refs'].forEach((type: string) => {
SetupProxyHandlers[type] = {
2019-08-23 10:07:51 +08:00
get: (instance, key) => (instance[type] as any)[key],
has: (instance, key) =>
key === SetupProxySymbol || key in (instance[type] as any),
ownKeys: instance => Reflect.ownKeys(instance[type] as any),
// this is necessary for ownKeys to work properly
getOwnPropertyDescriptor: (instance, key) =>
Reflect.getOwnPropertyDescriptor(instance[type], key),
2019-06-19 16:43:34 +08:00
set: () => false,
deleteProperty: () => false
}
})
function createSetupContext(instance: ComponentInstance): SetupContext {
const context = {
// attrs, slots & refs are non-reactive, but they need to always expose
// the latest values (instance.xxx may get replaced during updates) so we
// need to expose them through a proxy
attrs: new Proxy(instance, SetupProxyHandlers.attrs),
slots: new Proxy(instance, SetupProxyHandlers.slots),
refs: new Proxy(instance, SetupProxyHandlers.refs),
2019-08-27 06:08:56 +08:00
emit: instance.emit
2019-06-19 16:43:34 +08:00
} as any
return __DEV__ ? Object.freeze(context) : context
}
2019-09-01 04:36:36 +08:00
// mark the current rendering instance for asset resolution (e.g.
// resolveComponent, resolveDirective) during render
export let currentRenderingInstance: ComponentInstance | null = null
2019-05-29 11:36:16 +08:00
export function renderComponentRoot(instance: ComponentInstance): VNode {
2019-06-19 16:43:34 +08:00
const {
type: Component,
vnode,
renderProxy,
props,
slots,
attrs,
2019-08-27 06:08:56 +08:00
emit
2019-06-19 16:43:34 +08:00
} = instance
2019-09-01 04:36:36 +08:00
let result
currentRenderingInstance = instance
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
2019-09-01 04:36:36 +08:00
result = normalizeVNode(
(instance.render as RenderFunction).call(renderProxy)
)
} else {
// functional
const render = Component as FunctionalComponent
2019-09-01 04:36:36 +08:00
result = normalizeVNode(
render.length > 1
? render(props, {
attrs,
slots,
emit
})
: render(props, null as any)
)
}
} catch (err) {
2019-08-31 03:15:23 +08:00
handleError(err, instance, ErrorTypes.RENDER_FUNCTION)
2019-09-01 04:36:36 +08:00
result = createVNode(Empty)
2019-05-28 17:19:47 +08:00
}
2019-09-01 04:36:36 +08:00
currentRenderingInstance = null
return result
2019-05-28 13:27:31 +08:00
}
export function shouldUpdateComponent(
prevVNode: VNode,
2019-06-01 02:14:49 +08:00
nextVNode: VNode,
optimized?: boolean
2019-05-28 13:27:31 +08:00
): boolean {
const { props: prevProps, children: prevChildren } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
2019-06-03 09:43:28 +08:00
if (patchFlag) {
2019-08-22 23:12:37 +08:00
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
2019-05-30 16:00:42 +08:00
// slot content that references values that might have changed,
// e.g. in a v-for
return true
}
2019-08-22 23:12:37 +08:00
if (patchFlag & PatchFlags.FULL_PROPS) {
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps as Data, nextProps as Data)
2019-08-22 23:12:37 +08:00
} else if (patchFlag & PatchFlags.PROPS) {
2019-05-30 16:00:42 +08:00
const dynamicProps = nextVNode.dynamicProps as string[]
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if ((nextProps as any)[key] !== (prevProps as any)[key]) {
return true
}
}
}
2019-06-01 02:14:49 +08:00
} else if (!optimized) {
// this path is only taken by manually written render functions
// so presence of any children leads to a forced update
if (prevChildren != null || nextChildren != null) {
return true
}
2019-05-30 16:00:42 +08:00
if (prevProps === nextProps) {
return false
}
if (prevProps === null) {
return nextProps !== null
}
if (nextProps === null) {
return prevProps !== null
}
return hasPropsChanged(prevProps, nextProps)
}
return false
}
function hasPropsChanged(prevProps: Data, nextProps: Data): boolean {
const nextKeys = Object.keys(nextProps)
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) {
2019-05-28 13:27:31 +08:00
return true
}
}
return false
}
2019-09-03 04:43:26 +08:00
export function resolveComponent(name: string): Component | undefined {
2019-09-04 23:36:27 +08:00
return resolveAsset('components', name) as any
2019-09-03 04:43:26 +08:00
}