vue3-yuanma/packages/runtime-core/src/vnode.ts
2020-07-06 21:12:15 -04:00

598 lines
17 KiB
TypeScript

import {
isArray,
isFunction,
isString,
isObject,
EMPTY_ARR,
extend,
normalizeClass,
normalizeStyle,
PatchFlags,
ShapeFlags
} from '@vue/shared'
import {
ComponentInternalInstance,
Data,
Component,
ClassComponent
} from './component'
import { RawSlots } from './componentSlots'
import { isProxy, Ref, toRaw } from '@vue/reactivity'
import { AppContext } from './apiCreateApp'
import {
SuspenseImpl,
isSuspense,
SuspenseBoundary
} from './components/Suspense'
import { DirectiveBinding } from './directives'
import { TransitionHooks } from './components/BaseTransition'
import { warn } from './warning'
import { currentScopeId } from './helpers/scopeId'
import { TeleportImpl, isTeleport } from './components/Teleport'
import { currentRenderingInstance } from './componentRenderUtils'
import { RendererNode, RendererElement } from './renderer'
import { NULL_DYNAMIC_COMPONENT } from './helpers/resolveAssets'
import { hmrDirtyComponents } from './hmr'
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)
export type VNodeTypes =
| string
| VNode
| Component
| typeof Text
| typeof Static
| typeof Comment
| typeof Fragment
| typeof TeleportImpl
| typeof SuspenseImpl
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[]
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[]
}
type VNodeChildAtom =
| VNode
| string
| number
| boolean
| null
| undefined
| void
export interface VNodeArrayChildren
extends Array<VNodeArrayChildren | VNodeChildAtom> {}
export type VNodeChild = VNodeChildAtom | VNodeArrayChildren
export type VNodeNormalizedChildren =
| string
| VNodeArrayChildren
| RawSlots
| null
export interface VNode<HostNode = RendererNode, HostElement = RendererElement> {
/**
* @internal
*/
__v_isVNode: true
/**
* @internal
*/
__v_skip: true
type: VNodeTypes
props: VNodeProps | null
key: string | number | null
ref: VNodeNormalizedRef | null
scopeId: string | null // SFC only
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
suspense: SuspenseBoundary | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode
// optimization only
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
}
// 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).
const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
/**
* 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:
*
* ```js
* function render() {
* return (openBlock(),createBlock('div', null, [...]))
* }
* ```
* disableTracking is true when creating a v-for fragment block, since a v-for
* fragment always diffs its children.
*
* @private
*/
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
// 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:
*
* ``` js
* _cache[1] || (
* setBlockTracking(-1),
* _cache[1] = createVNode(...),
* setBlockTracking(1),
* _cache[1]
* )
* ```
*
* @private
*/
export function setBlockTracking(value: number) {
shouldTrack += value
}
/**
* 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.
*
* @private
*/
export function createBlock(
type: VNodeTypes | ClassComponent,
props?: { [key: string]: any } | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
const vnode = createVNode(
type,
props,
children,
patchFlag,
dynamicProps,
true /* isBlock: prevent a block from tracking itself */
)
// 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
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
export function isVNode(value: any): value is VNode {
return value ? value.__v_isVNode === true : false
}
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
if (
__DEV__ &&
n2.shapeFlag & ShapeFlags.COMPONENT &&
hmrDirtyComponents.has(n2.type as Component)
) {
// 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
* It is *internal* but needs to be exposed for test-utils to pick up proper
* typings
*/
export function transformVNodeArgs(transformer?: typeof vnodeArgsTransformer) {
vnodeArgsTransformer = transformer
}
const createVNodeWithArgsTransform = (
...args: Parameters<typeof _createVNode>
): VNode => {
return _createVNode(
...(vnodeArgsTransformer
? vnodeArgsTransformer(args, currentRenderingInstance)
: args)
)
}
export const InternalObjectKey = `__vInternal`
const normalizeKey = ({ key }: VNodeProps): VNode['key'] =>
key != null ? key : null
const normalizeRef = ({ ref }: VNodeProps): VNode['ref'] => {
return (ref != null
? isArray(ref)
? ref
: [currentRenderingInstance!, ref]
: null) as any
}
export const createVNode = (__DEV__
? createVNodeWithArgsTransform
: _createVNode) as typeof _createVNode
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
if (isVNode(type)) {
return cloneVNode(type, props, children)
}
// class component normalization.
if (isFunction(type) && '__vccOpts' in type) {
type = type.__vccOpts
}
// class & style normalization.
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
if (isProxy(props) || InternalObjectKey in props) {
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// encode the vnode type information into a bitmap
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
type = toRaw(type)
warn(
`Vue received a Component which was made a reactive object. This can ` +
`lead to unnecessary performance overhead, and should be avoided by ` +
`marking the component with \`markRaw\` or using \`shallowRef\` ` +
`instead of \`ref\`.`,
`\nComponent that was made reactive: `,
type
)
}
const vnode: VNode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
children: null,
component: null,
suspense: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {
warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
normalizeChildren(vnode, children)
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// 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.
if (
shouldTrack > 0 &&
!isBlockNode &&
currentBlock &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
patchFlag !== PatchFlags.HYDRATE_EVENTS &&
(patchFlag > 0 ||
shapeFlag & ShapeFlags.SUSPENSE ||
shapeFlag & ShapeFlags.TELEPORT ||
shapeFlag & ShapeFlags.STATEFUL_COMPONENT ||
shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
) {
currentBlock.push(vnode)
}
return vnode
}
export function cloneVNode<T, U>(
vnode: VNode<T, U>,
extraProps?: Data & VNodeProps | null,
children?: unknown
): VNode<T, U> {
const props = extraProps
? vnode.props
? mergeProps(vnode.props, extraProps)
: extend({}, extraProps)
: vnode.props
// This is intentionally NOT using spread or extend to avoid the runtime
// key enumeration cost.
const cloned: VNode<T, U> = {
__v_isVNode: true,
__v_skip: true,
type: vnode.type,
props,
key: props && normalizeKey(props),
ref: extraProps && extraProps.ref ? normalizeRef(extraProps) : vnode.ref,
scopeId: vnode.scopeId,
children: vnode.children,
target: vnode.target,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
// if the vnode is cloned with extra props, we can no longer assume its
// existing patch flag to be reliable and need to bail out of optimized mode.
// however we don't want block nodes to de-opt their children, so if the
// vnode is a block node, we only add the FULL_PROPS flag to it.
patchFlag: extraProps
? vnode.dynamicChildren
? vnode.patchFlag | PatchFlags.FULL_PROPS
: PatchFlags.BAIL
: vnode.patchFlag,
dynamicProps: vnode.dynamicProps,
dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
dirs: vnode.dirs,
transition: vnode.transition,
// 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
}
if (children) {
normalizeChildren(cloned, children)
}
return cloned
}
/**
* @private
*/
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
return createVNode(Text, null, text, flag)
}
/**
* @private
*/
export function createStaticVNode(
content: string,
numberOfNodes: number
): VNode {
// A static vnode can contain multiple stringified elements, and the number
// of elements is necessary for hydration.
const vnode = createVNode(Static, null, content)
vnode.staticCount = numberOfNodes
return vnode
}
/**
* @private
*/
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') {
// empty placeholder
return createVNode(Comment)
} else if (isArray(child)) {
// fragment
return createVNode(Fragment, null, child)
} else if (typeof child === 'object') {
// already vnode, this should be the most common since compiled templates
// always produce all-vnode children arrays
return child.el === null ? child : cloneVNode(child)
} else {
// strings and numbers
return createVNode(Text, null, String(child))
}
}
// optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode {
return child.el === null ? child : cloneVNode(child)
}
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const { shapeFlag } = vnode
if (children == null) {
children = null
} else if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') {
// Normalize slot to plain children
if (
(shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) &&
(children as any).default
) {
normalizeChildren(vnode, (children as any).default())
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
if (!(children as RawSlots)._ && !(InternalObjectKey in children!)) {
// if slots are not normalized, attach context instance
// (compiled / normalized slots already have context)
;(children as RawSlots)._ctx = currentRenderingInstance
}
}
} else if (isFunction(children)) {
children = { default: children, _ctx: currentRenderingInstance }
type = ShapeFlags.SLOTS_CHILDREN
} else {
children = String(children)
// force teleport children to array so it can be moved around
if (shapeFlag & ShapeFlags.TELEPORT) {
type = ShapeFlags.ARRAY_CHILDREN
children = [createTextVNode(children as string)]
} else {
type = ShapeFlags.TEXT_CHILDREN
}
}
vnode.children = children as VNodeNormalizedChildren
vnode.shapeFlag |= type
}
const handlersRE = /^on|^vnode/
export function mergeProps(...args: (Data & VNodeProps)[]) {
const ret = extend({}, args[0])
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])
}
} 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])
: incoming
}
} else {
ret[key] = toMerge[key]
}
}
}
return ret
}