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

479 lines
14 KiB
TypeScript
Raw Normal View History

2019-08-23 02:07:51 +00:00
import {
isArray,
isFunction,
isString,
isObject,
EMPTY_ARR,
2020-01-27 22:23:42 +00:00
extend,
normalizeClass,
2020-02-13 22:47:00 +00:00
normalizeStyle,
PatchFlags,
ShapeFlags
2019-08-23 02:07:51 +00:00
} from '@vue/shared'
2019-10-08 13:26:09 +00:00
import {
ComponentInternalInstance,
Data,
SetupProxySymbol,
Component,
ClassComponent
2019-10-08 13:26:09 +00:00
} from './component'
2019-05-31 10:07:43 +00:00
import { RawSlots } from './componentSlots'
2019-11-01 13:58:27 +00:00
import { isReactive, Ref } from '@vue/reactivity'
2019-12-22 17:25:04 +00:00
import { AppContext } from './apiCreateApp'
2020-02-15 16:40:09 +00:00
import {
SuspenseImpl,
isSuspense,
SuspenseBoundary
} from './components/Suspense'
import { DirectiveBinding } from './directives'
2019-11-24 21:00:46 +00:00
import { TransitionHooks } from './components/BaseTransition'
import { warn } from './warning'
2019-12-16 18:33:10 +00:00
import { currentScopeId } from './helpers/scopeId'
2020-02-15 16:40:09 +00:00
import { PortalImpl, isPortal } from './components/Portal'
import { currentRenderingInstance } from './componentRenderUtils'
import { RendererNode, RendererElement } from './renderer'
2019-05-28 02:28:25 +00:00
export const Fragment = (Symbol(__DEV__ ? 'Fragment' : undefined) as any) as {
__isFragment: true
new (): {
$props: VNodeProps
}
}
export const Text = Symbol(__DEV__ ? 'Text' : undefined)
export const Comment = Symbol(__DEV__ ? 'Comment' : undefined)
export const Static = Symbol(__DEV__ ? 'Static' : undefined)
2018-09-19 15:35:38 +00:00
2019-08-23 19:27:17 +00:00
export type VNodeTypes =
2019-05-25 15:51:20 +00:00
| string
2019-10-08 13:26:09 +00:00
| Component
2019-05-25 15:51:20 +00:00
| typeof Text
| typeof Static
| typeof Comment
2020-02-15 16:40:09 +00:00
| typeof Fragment
| typeof PortalImpl
| typeof SuspenseImpl
2018-09-19 15:35:38 +00:00
export type VNodeRef =
| string
| Ref
| ((ref: object | null, refs: Record<string, any>) => void)
export type VNodeNormalizedRef = [ComponentInternalInstance, VNodeRef]
type VNodeMountHook = (vnode: VNode) => void
type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
export type VNodeHook =
| VNodeMountHook
| VNodeUpdateHook
| VNodeMountHook[]
| VNodeUpdateHook[]
2019-11-01 13:58:27 +00:00
export interface VNodeProps {
[key: string]: any
key?: string | number
ref?: VNodeRef
// vnode hooks
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
2019-11-01 13:58:27 +00:00
}
type VNodeChildAtom = VNode | string | number | boolean | null | void
2019-09-06 20:58:32 +00:00
export interface VNodeArrayChildren<
HostNode = RendererNode,
HostElement = RendererElement
> extends Array<VNodeArrayChildren | VNodeChildAtom> {}
2018-10-12 23:49:41 +00:00
export type VNodeChild = VNodeChildAtom | VNodeArrayChildren
2019-09-06 20:58:32 +00:00
export type VNodeNormalizedChildren =
2019-09-06 20:58:32 +00:00
| string
| VNodeArrayChildren
2019-09-06 20:58:32 +00:00
| RawSlots
| null
2019-05-31 10:07:43 +00:00
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
_isVNode: true
2019-05-25 15:51:20 +00:00
type: VNodeTypes
2019-11-01 13:58:27 +00:00
props: VNodeProps | null
2019-05-25 15:51:20 +00:00
key: string | number | null
ref: VNodeNormalizedRef | null
2019-12-16 18:33:10 +00:00
scopeId: string | null // SFC only
children: VNodeNormalizedChildren
2019-09-06 16:58:31 +00:00
component: ComponentInternalInstance | null
suspense: SuspenseBoundary | null
dirs: DirectiveBinding[] | null
2019-11-22 22:10:17 +00:00
transition: TransitionHooks | null
2019-05-28 05:27:31 +00:00
// DOM
2019-05-28 09:19:47 +00:00
el: HostNode | null
anchor: HostNode | null // fragment anchor
2019-09-06 20:58:32 +00:00
target: HostElement | null // portal target
2019-05-28 05:27:31 +00:00
// optimization only
2019-06-02 08:35:19 +00:00
shapeFlag: number
2019-06-01 09:43:41 +00:00
patchFlag: number
2019-05-25 15:51:20 +00:00
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
2019-09-02 20:09:34 +00:00
// application root node only
appContext: AppContext | null
2018-10-12 23:49:41 +00:00
}
2019-05-30 13:24:40 +00:00
// Since v-if and v-for are the two possible ways node structure can dynamically
// change, once we consider v-if branches and each v-for fragment a block, we
// can divide a template into nested blocks, and within each block the node
// structure would be stable. This allows us to skip most children diffing
// and only worry about the dynamic nodes (indicated by patch flags).
2019-05-27 05:48:40 +00:00
const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
2018-09-19 15:35:38 +00:00
2019-05-30 13:24:40 +00:00
// Open a block.
// This must be called before `createBlock`. It cannot be part of `createBlock`
// because the children of the block are evaluated before `createBlock` itself
// is called. The generated code typically looks like this:
//
// function render() {
// return (openBlock(),createBlock('div', null, [...]))
// }
2019-06-03 05:44:45 +00:00
//
// disableTracking is true when creating a fragment block, since a fragment
// always diffs its children.
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
2019-05-25 15:51:20 +00:00
}
2018-10-12 23:49:41 +00:00
// Whether we should be tracking dynamic child nodes inside a block.
// Only tracks when this value is > 0
// We are not using a simple boolean because this value may need to be
// incremented/decremented by nested usage of v-once (see below)
let shouldTrack = 1
// Block tracking sometimes needs to be disabled, for example during the
// creation of a tree that needs to be cached by v-once. The compiler generates
// code like this:
// _cache[1] || (
// setBlockTracking(-1),
// _cache[1] = createVNode(...),
// setBlockTracking(1),
// _cache[1]
// )
export function setBlockTracking(value: number) {
shouldTrack += value
}
2018-10-12 23:49:41 +00:00
2019-05-30 13:24:40 +00:00
// Create a block root vnode. Takes the same exact arguments as `createVNode`.
// A block root keeps track of dynamic nodes within the block in the
// `dynamicChildren` array.
2019-05-25 15:51:20 +00:00
export function createBlock(
type: VNodeTypes | ClassComponent,
2019-05-25 15:51:20 +00:00
props?: { [key: string]: any } | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
// avoid a block with patchFlag tracking itself
shouldTrack--
2019-05-25 15:51:20 +00:00
const vnode = createVNode(type, props, children, patchFlag, dynamicProps)
shouldTrack++
// save current block children on the block vnode
vnode.dynamicChildren = currentBlock || EMPTY_ARR
// close block
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1] || null
// a block is always going to be patched, so track it as a child of its
// parent block
2020-03-18 22:14:51 +00:00
if (currentBlock) {
currentBlock.push(vnode)
}
2019-05-25 15:51:20 +00:00
return vnode
2018-10-12 17:42:19 +00:00
}
export function isVNode(value: any): value is VNode {
return value ? value._isVNode === true : false
}
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
if (
__BUNDLER__ &&
__DEV__ &&
n2.shapeFlag & ShapeFlags.COMPONENT &&
(n2.type as Component).__hmrUpdated
) {
// HMR only: if the component has been hot-updated, force a reload.
return false
}
return n1.type === n2.type && n1.key === n2.key
}
let vnodeArgsTransformer:
| ((
args: Parameters<typeof _createVNode>,
instance: ComponentInternalInstance | null
) => Parameters<typeof _createVNode>)
| undefined
// Internal API for registering an arguments transform for createVNode
// used for creating stubs in the test-utils
export function transformVNodeArgs(transformer?: typeof vnodeArgsTransformer) {
vnodeArgsTransformer = transformer
}
const createVNodeWithArgsTransform = (
...args: Parameters<typeof _createVNode>
): VNode => {
return _createVNode(
...(vnodeArgsTransformer
? vnodeArgsTransformer(args, currentRenderingInstance)
: args)
)
}
2020-03-23 21:36:19 +00:00
export const createVNode = (__DEV__
? createVNodeWithArgsTransform
: _createVNode) as typeof _createVNode
function _createVNode(
type: VNodeTypes | ClassComponent,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
2019-06-01 09:43:41 +00:00
patchFlag: number = 0,
2019-05-25 15:51:20 +00:00
dynamicProps: string[] | null = null
): VNode {
if (!type) {
if (__DEV__) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
2019-11-24 21:00:46 +00:00
type = Comment
}
// class component normalization.
if (isFunction(type) && '__vccOpts' in type) {
type = type.__vccOpts
}
2019-08-23 02:07:51 +00:00
// class & style normalization.
2020-03-18 22:14:51 +00:00
if (props) {
2019-08-23 02:07:51 +00:00
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isReactive(props) || SetupProxySymbol in props) {
props = extend({}, props)
}
let { class: klass, style } = props
2020-03-18 22:14:51 +00:00
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
2019-08-23 02:07:51 +00:00
}
if (isObject(style)) {
2019-08-23 02:07:51 +00:00
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isReactive(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
2019-06-02 14:22:44 +00:00
// encode the vnode type information into a bitmap
2019-08-22 15:12:37 +00:00
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
2020-02-15 16:40:09 +00:00
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
2020-02-15 16:40:09 +00:00
: isPortal(type)
? ShapeFlags.PORTAL
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
2019-05-25 15:51:20 +00:00
const vnode: VNode = {
_isVNode: true,
2019-05-25 15:51:20 +00:00
type,
2019-06-01 09:43:41 +00:00
props,
2020-03-18 22:14:51 +00:00
key: props && props.key !== undefined ? props.key : null,
ref:
2020-03-18 22:14:51 +00:00
props && props.ref !== undefined
? [currentRenderingInstance!, props.ref]
: null,
2019-12-16 18:33:10 +00:00
scopeId: currentScopeId,
2019-06-02 14:22:44 +00:00
children: null,
2019-05-28 05:27:31 +00:00
component: null,
2019-09-07 15:28:40 +00:00
suspense: null,
dirs: null,
transition: null,
2019-05-27 05:48:40 +00:00
el: null,
anchor: null,
2019-05-29 08:10:25 +00:00
target: null,
2019-08-22 15:12:37 +00:00
shapeFlag,
2019-05-25 15:51:20 +00:00
patchFlag,
dynamicProps,
2019-09-02 20:09:34 +00:00
dynamicChildren: null,
appContext: null
2018-09-19 15:35:38 +00:00
}
2019-06-01 09:43:41 +00:00
2019-06-02 14:22:44 +00:00
normalizeChildren(vnode, children)
2019-09-01 02:17:46 +00:00
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
2019-05-31 18:14:49 +00:00
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
2019-06-02 08:35:19 +00:00
if (
shouldTrack > 0 &&
2020-03-18 22:14:51 +00:00
currentBlock &&
2020-02-13 22:47:00 +00:00
// the EVENTS flag is only for hydration and if it is the only flag, the
2020-02-13 23:28:40 +00:00
// vnode should not be considered dynamic due to handler caching.
2020-02-13 22:47:00 +00:00
patchFlag !== PatchFlags.HYDRATE_EVENTS &&
(patchFlag > 0 ||
shapeFlag & ShapeFlags.SUSPENSE ||
2019-08-22 15:12:37 +00:00
shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
2019-06-02 08:35:19 +00:00
) {
currentBlock.push(vnode)
2018-09-24 23:11:14 +00:00
}
2019-06-01 09:43:41 +00:00
2019-05-25 15:51:20 +00:00
return vnode
}
2018-09-19 15:35:38 +00:00
export function cloneVNode<T, U>(
vnode: VNode<T, U>,
extraProps?: Data & VNodeProps
): VNode<T, U> {
// This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost.
2019-08-22 21:12:39 +00:00
return {
_isVNode: true,
2019-08-22 21:12:39 +00:00
type: vnode.type,
props: extraProps
? vnode.props
? mergeProps(vnode.props, extraProps)
: extraProps
: vnode.props,
2019-08-22 21:12:39 +00:00
key: vnode.key,
ref: vnode.ref,
2019-12-16 18:33:10 +00:00
scopeId: vnode.scopeId,
2019-09-02 16:09:29 +00:00
children: vnode.children,
target: vnode.target,
2019-08-22 21:12:39 +00:00
shapeFlag: vnode.shapeFlag,
patchFlag: vnode.patchFlag,
dynamicProps: vnode.dynamicProps,
2019-09-02 16:09:29 +00:00
dynamicChildren: vnode.dynamicChildren,
2019-09-02 20:09:34 +00:00
appContext: vnode.appContext,
dirs: vnode.dirs,
transition: vnode.transition,
2019-09-02 16:09:29 +00:00
2019-10-30 02:28:38 +00:00
// These should technically only be non-null on mounted VNodes. However,
// they *should* be copied for kept-alive vnodes. So we just always copy
// them since them being non-null during a mount doesn't affect the logic as
// they will simply be overwritten.
component: vnode.component,
suspense: vnode.suspense,
el: vnode.el,
anchor: vnode.anchor
2019-08-22 21:12:39 +00:00
}
2019-05-25 15:51:20 +00:00
}
2019-05-28 05:27:31 +00:00
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
return createVNode(Text, null, text, flag)
}
export function createStaticVNode(content: string): VNode {
return createVNode(Static, null, content)
}
export function createCommentVNode(
text: string = '',
// when used as the v-else branch, the comment node must be created as a
// block to ensure correct updates.
asBlock: boolean = false
): VNode {
return asBlock
? (openBlock(), createBlock(Comment, null, text))
: createVNode(Comment, null, text)
}
export function normalizeVNode(child: VNodeChild): VNode {
if (child == null || typeof child === 'boolean') {
2019-05-28 05:27:31 +00:00
// empty placeholder
return createVNode(Comment)
2019-05-28 05:27:31 +00:00
} else if (isArray(child)) {
// fragment
return createVNode(Fragment, null, child)
} else if (typeof child === 'object') {
2019-05-31 10:07:43 +00:00
// already vnode, this should be the most common since compiled templates
// always produce all-vnode children arrays
2019-08-22 21:12:39 +00:00
return child.el === null ? child : cloneVNode(child)
2019-05-28 05:27:31 +00:00
} else {
// strings and numbers
return createVNode(Text, null, String(child))
2019-05-28 05:27:31 +00:00
}
}
2019-05-31 10:07:43 +00:00
// optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode {
2019-12-22 21:24:24 +00:00
return child.el === null ? child : cloneVNode(child)
}
2019-06-02 14:22:44 +00:00
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
2019-05-31 10:07:43 +00:00
if (children == null) {
2019-06-02 14:22:44 +00:00
children = null
2019-05-31 10:07:43 +00:00
} else if (isArray(children)) {
2019-08-22 15:12:37 +00:00
type = ShapeFlags.ARRAY_CHILDREN
2019-05-31 10:07:43 +00:00
} else if (typeof children === 'object') {
// in case <component :is="x"> resolves to native element, the vnode call
// will receive slots object.
if (vnode.shapeFlag & ShapeFlags.ELEMENT && (children as any).default) {
normalizeChildren(vnode, (children as any).default())
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
if (!(children as RawSlots)._) {
;(children as RawSlots)._ctx = currentRenderingInstance
}
}
2019-05-31 10:07:43 +00:00
} else if (isFunction(children)) {
children = { default: children, _ctx: currentRenderingInstance }
2019-08-22 15:12:37 +00:00
type = ShapeFlags.SLOTS_CHILDREN
2019-05-31 10:07:43 +00:00
} else {
children = String(children)
2019-08-22 15:12:37 +00:00
type = ShapeFlags.TEXT_CHILDREN
2019-05-31 10:07:43 +00:00
}
vnode.children = children as VNodeNormalizedChildren
2019-06-02 14:22:44 +00:00
vnode.shapeFlag |= type
2019-05-31 10:07:43 +00:00
}
2019-06-01 09:43:41 +00:00
2019-08-22 21:12:39 +00:00
const handlersRE = /^on|^vnode/
export function mergeProps(...args: (Data & VNodeProps)[]) {
2019-08-22 21:12:39 +00:00
const ret: Data = {}
2019-08-23 02:07:51 +00:00
extend(ret, args[0])
2019-08-22 21:12:39 +00:00
for (let i = 1; i < args.length; i++) {
const toMerge = args[i]
for (const key in toMerge) {
if (key === 'class') {
if (ret.class !== toMerge.class) {
ret.class = normalizeClass([ret.class, toMerge.class])
}
2019-08-22 21:12:39 +00:00
} else if (key === 'style') {
ret.style = normalizeStyle([ret.style, toMerge.style])
} else if (handlersRE.test(key)) {
// on*, vnode*
const existing = ret[key]
const incoming = toMerge[key]
if (existing !== incoming) {
ret[key] = existing
? [].concat(existing as any, toMerge[key] as any)
: incoming
}
2019-08-22 21:12:39 +00:00
} else {
ret[key] = toMerge[key]
}
}
}
return ret
}