【vue3 源码】十二、意识虚构 DOM
什么是虚构 DOM?
虚构 DOM(也能够称为vnode
)形容了一个实在的 DOM 构造,它和实在 DOM 一样都是由很多节点组成的一个树形构造。实质其实就是一个 JS 对象,如下就是一个vnode
:
{
type: 'div',
props: {id: 'container'},
children: [
{
type: 'span',
props: {class: 'span1'},
children: 'Hello'
},
{
type: 'span',
props: {class: 'span2'},
children: 'World'
},
]
}
下面这个 vnode
形容的实在 DOM 构造如下:
<div id="container">
<span class="text1">Hello </span>
<span class="text2">World</span>
</div>
能够发现,虚构节点的 type
形容了标签的类型,props
形容了标签的属性,children
形容了标签的子节点。当然一个 vnode
不仅只有这三个属性。vue3
中对 vnode
的类型定义如下:
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = {[key: string]: any }
> {
// 标记为一个 VNode
__v_isVNode: true
// 禁止将 VNode 解决为响应式对象
[ReactiveFlags.SKIP]: true
// 节点类型
type: VNodeTypes
// 节点的属性
props: (VNodeProps & ExtraProps) | null
// 便与 DOM 的复用,次要用在 diff 算法中
key: string | number | symbol | null
// 被用来给元素或子组件注册援用信息
ref: VNodeNormalizedRef | null
scopeId: string | null
slotScopeIds: string[] | null
// 子节点
children: VNodeNormalizedChildren
// 组件实例
component: ComponentInternalInstance | null
// 指令信息
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
// vnode 对应的 DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
// teleport 须要挂载的指标 DOM
target: HostElement | null
// teleport 挂载所需的锚点
targetAnchor: HostNode | null
// 对于 Static vnode 所蕴含的动态节点数量
staticCount: number
// suspense 组件的边界
suspense: SuspenseBoundary | null
// suspense 的 default slot 对应的 vnode
ssContent: VNode | null
// suspense 的 fallback slot 对应的 vnode
ssFallback: VNode | null
// 用于优化的标记,次要用于判断节点类型
shapeFlag: number
// 用于 diff 优化的补丁标记
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
/**
* @internal attached by v-memo
*/
memo?: any[]
/**
* @internal __COMPAT__ only
*/
isCompatRoot?: true
/**
* @internal custom element interception hook
*/
ce?: (instance: ComponentInternalInstance) => void
}
如何创立虚构 DOM
vue3
对外提供了 h()
办法用于创立虚构 DOM。所在文件门路:packages/runtime-core/src/h.ts
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
const l = arguments.length
if (l === 2) {
// propsOrChildren 是对象且不是数组
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// propsOrChildren 是 vnode
if (isVNode(propsOrChildren)) {return createVNode(type, null, [propsOrChildren])
}
// 有 props 无子节点
return createVNode(type, propsOrChildren)
} else {
// 有子节点
return createVNode(type, null, propsOrChildren)
}
} else {
// 如果参数大于 3,那么第三个参数及之后的参数都会被作为子节点解决
if (l > 3) {children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
在 h
函数会应用 createVNode
函数创立虚构 DOM。
export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode
能够看到 createVNode
在开发环境下会应用 createVNodeWithArgsTransform
,其余环境下会应用_createVNode
。这里咱们只看下_createVNode
的实现。
<details>
<summary>_createVNode
残缺代码 </summary>
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
}
// 如果 type 曾经是个 vnode,则复制个新的 vnode
if (isVNode(type)) {const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {normalizeChildren(cloned, children)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {if (cloned.shapeFlag & ShapeFlags.COMPONENT) {currentBlock[currentBlock.indexOf(type)] = cloned
} else {currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned
}
// class 组件的 type
if (isClassComponent(type)) {type = type.__vccOpts}
// 兼容 2.x 的异步及函数式组件
if (__COMPAT__) {type = convertLegacyComponent(type, currentRenderingInstance)
}
// class、style 的标准化
if (props) {props = guardReactiveProps(props)!
let {class: klass, style} = props
if (klass && !isString(klass)) {props.class = normalizeClass(klass)
}
if (isObject(style)) {if (isProxy(style) && !isArray(style)) {style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 依据 type 属性确定 patchFlag
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
)
}
return createBaseVNode(
type,
props,
children,
patchFlag,
dynamicProps,
shapeFlag,
isBlockNode,
true
)
}
</details>
_createVNode
能够承受 6 个参数:
type
:vnode
类型props
:vnode
的属性children
:子vnode
patchFlag
:补丁标记,由编译器生成vnode
时的优化提醒,在 diff 期间会进入对应优化dynamicProps
:动静属性isBlockNode
:是否是个Block
节点
首先会对 type
进行校验,如果 type
是空的动静组件,进行提醒,并将 type
指定为一个 Comment
正文 DOM。
if (!type || type === NULL_DYNAMIC_COMPONENT) {if (__DEV__ && !type) {warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
如果 type
曾经是个 vnode
,会从type
复制出一个新的 vnode
。这种状况次要在<component :is="vnode"/>
状况下产生
if (isVNode(type)) {const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
// 批改其 children 属性及欠缺 shapeFlag 属性
normalizeChildren(cloned, children)
}
// 将被拷贝的对象存入 currentBlock 中
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {if (cloned.shapeFlag & ShapeFlags.COMPONENT) {currentBlock[currentBlock.indexOf(type)] = cloned
} else {currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned
}
对于 cloneVNode
的实现:
export function cloneVNode<T, U>(
vnode: VNode<T, U>,
extraProps?: (Data & VNodeProps) | null,
mergeRef = false
): VNode<T, U> {const { props, ref, patchFlag, children} = vnode
// 如果存在 extraProps,须要将 extraProps 和 vnode 的 props 进行合并
const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props
const cloned: VNode = {
__v_isVNode: true,
__v_skip: true,
type: vnode.type,
props: mergedProps,
// 如果过 mergedProps 中不存在 key,则设置为 null
key: mergedProps && normalizeKey(mergedProps),
// 如果过存在额定的 ref
// 如果过须要合并 ref
// 如果被拷贝节点中的 ref 是个数组,将调用 normalizeRef 解决 ref,并将后果合并到被拷贝节点中的 ref 中
// 否则,创立一个新的数组,存储 ref 和 normalizeRef(extraProps)的后果
// 否则间接调用 normalizeRef(extraProps)解决新的 ref
// 否则 ref 不变
ref:
extraProps && extraProps.ref
? mergeRef && ref
? isArray(ref)
? ref.concat(normalizeRef(extraProps)!)
: [ref, normalizeRef(extraProps)!]
: normalizeRef(extraProps)
: ref,
scopeId: vnode.scopeId,
slotScopeIds: vnode.slotScopeIds,
children:
__DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children)
? (children as VNode[]).map(deepCloneVNode)
: children,
target: vnode.target,
targetAnchor: vnode.targetAnchor,
staticCount: vnode.staticCount,
shapeFlag: vnode.shapeFlag,
// 如果 vnode 应用额定的 props 克隆,咱们不能再假如其现有的补丁标记是牢靠的,须要增加 FULL_PROPS 标记
// 如果存在 extraProps,并且 vnode.type 不是是 Fragment 片段的状况下:// 如果 patchFlag 为 -1,阐明是动态节点,它的内容不会发生变化。新的 vnode 的 patchFlag 为 PatchFlags.FULL_PROPS,示意 props 中存在动静 key
// 如果 patchFlag 不为 -1,将 patchFlag 与 PatchFlags.FULL_PROPS 进行或运算
// 否则 patchFlag 放弃不变
patchFlag:
extraProps && vnode.type !== Fragment
? patchFlag === -1 // hoisted node
? PatchFlags.FULL_PROPS
: patchFlag | PatchFlags.FULL_PROPS
: patchFlag,
dynamicProps: vnode.dynamicProps,
dynamicChildren: vnode.dynamicChildren,
appContext: vnode.appContext,
dirs: vnode.dirs,
transition: vnode.transition,
component: vnode.component,
suspense: vnode.suspense,
ssContent: vnode.ssContent && cloneVNode(vnode.ssContent),
ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback),
el: vnode.el,
anchor: vnode.anchor
}
// 用于兼容 vue2
if (__COMPAT__) {defineLegacyVNodeProperties(cloned)
}
return cloned as any
}
在复制节点的过程中次要解决经验以下步骤:
- 被拷贝节点的
props
与额定的props
的合并 -
创立新的
vnode
key
的解决:取合并后的props
中的key
,如果不存在,取null
ref
的合并:依据是否须要合并ref
,决定是否合并ref
patchFlag
的解决:如果vnode
应用额定的props
克隆,补丁标记不再牢靠的,须要增加FULL_PROPS
标记ssContent
的解决:应用cloneVNode
复制被拷贝节点的ssContent
ssFallback
的解决:应用cloneVNode
复制被拷贝节点的ssFallback
- 兼容
vue2
- 返回新的
vnode
在克隆 vnode
时,props
会应用 mergeProps
进行合并:
export function mergeProps(...args: (Data & VNodeProps)[]) {const ret: Data = {}
for (let i = 0; i < args.length; i++) {const toMerge = args[i]
for (const key in toMerge) {if (key === 'class') {if (ret.class !== toMerge.class) {
// 建设一个数组并调用 normalizeClass,最终 class 会是字符串的模式
ret.class = normalizeClass([ret.class, toMerge.class])
}
} else if (key === 'style') {
// 建设 style 数组并调用 normalizeStyle,最终 style 是对象模式
ret.style = normalizeStyle([ret.style, toMerge.style])
} else if (isOn(key)) { // 以 on 结尾的属性,对立按事件处理
const existing = ret[key]
const incoming = toMerge[key]
// 如果曾经存在的 key 对应事件与 incoming 不同,并且曾经存在的 key 对应事件中不蕴含 incoming
if (
incoming &&
existing !== incoming &&
!(isArray(existing) && existing.includes(incoming))
) {
// 如果过存在 existing,将 existing、incoming 合并到一个新的数组中
ret[key] = existing
? [].concat(existing as any, incoming as any)
: incoming
}
} else if (key !== '') {// 其余状况间接对 ret[key]进行赋值,靠后合并的值会取代之前的值
ret[key] = toMerge[key]
}
}
}
return ret
}
对于 normalizeClass
、normalizeStyle
的实现:
export function normalizeClass(value: unknown): string {
let res = ''
if (isString(value)) {res = value} else if (isArray(value)) {for (let i = 0; i < value.length; i++) {const normalized = normalizeClass(value[i])
if (normalized) {res += normalized + ' '}
}
} else if (isObject(value)) {for (const name in value) {if (value[name]) {res += name + ' '}
}
}
return res.trim()}
export function normalizeStyle(value: unknown): NormalizedStyle | string | undefined {if (isArray(value)) {const res: NormalizedStyle = {}
for (let i = 0; i < value.length; i++) {const item = value[i]
const normalized = isString(item)
? parseStringStyle(item)
: (normalizeStyle(item) as NormalizedStyle)
if (normalized) {for (const key in normalized) {res[key] = normalized[key]
}
}
}
return res
} else if (isString(value)) {return value} else if (isObject(value)) {return value}
}
const listDelimiterRE = /;(?![^(]*\))/g
const propertyDelimiterRE = /:(.+)/
export function parseStringStyle(cssText: string): NormalizedStyle {const ret: NormalizedStyle = {}
cssText.split(listDelimiterRE).forEach(item => {if (item) {const tmp = item.split(propertyDelimiterRE)
tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())
}
})
return ret
}
回到 _createVNode
中,当复制出一个新的 vnode
后,调用了一个 normalizeChildren
办法,该办法的作用是对新复制的 vnode
,批改其children
属性及欠缺 shapeFlag
属性
export function normalizeChildren(vnode: VNode, children: unknown) {
let type = 0
const {shapeFlag} = vnode
// 如果 children 为 null 或 undefined,children 取 null
if (children == null) {children = null} else if (isArray(children)) {
// 如果过 children 数数组,type 改为 ShapeFlags.ARRAY_CHILDREN
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'object') { // 如果 children 是对象
// 如果 vndoe 是 element 或 teleport
if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) {
// 取默认插槽
const slot = (children as any).default
if (slot) {// _c 标记由 withCtx() 增加,示意这是一个已编译的插槽
slot._c && (slot._d = false)
// 将默认插槽的后果作为 vnode 的 children
normalizeChildren(vnode, slot())
slot._c && (slot._d = true)
}
return
} else {
type = ShapeFlags.SLOTS_CHILDREN
const slotFlag = (children as RawSlots)._
if (!slotFlag && !(InternalObjectKey in children!)) {
// 如果槽未规范化,则附加上下文实例(编译过或规范话的 slots 曾经有上下文);(children as RawSlots)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
// 子组件接管来自父组件的转发 slots。// 它的插槽类型由其父插槽类型决定。if ((currentRenderingInstance.slots as RawSlots)._ === SlotFlags.STABLE
) {;(children as RawSlots)._ = SlotFlags.STABLE
} else {;(children as RawSlots)._ = SlotFlags.DYNAMIC
vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
}
}
}
} else if (isFunction(children)) { // 如果过 children 是 function
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
}
而后判断 vnode
是否应该被收集到 Block
中,并返回拷贝的节点。
如果 type
不是 vnode
,在办法最初会调用一个createBaseVNode
创立vnode
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
} as VNode
if (needFullChildrenNormalization) {normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// validate key
if (__DEV__ && vnode.key !== vnode.key) {warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
}
// 收集 vnode 到 block 树中
if (
isBlockTreeEnabled > 0 &&
// 防止 block 本人收集本人
!isBlockNode &&
// 存在父 block
currentBlock &&
// vnode.patchFlag 须要大于 0 或 shapeFlag 中存在 ShapeFlags.COMPONENT
// patchFlag 的存在表明该节点须要修补更新。// 组件节点也应该总是打补丁,因为即便组件不须要更新,它也须要将实例长久化到下一个 vnode,以便当前能够正确卸载它
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// 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.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {currentBlock.push(vnode)
}
if (__COMPAT__) {convertLegacyVModelProps(vnode)
defineLegacyVNodeProperties(vnode)
}
return vnode
}
总结
虚构 DOM
的创立流程:
- 如果
type
是个空的动静组件,将vnode.type
指定为Comment
正文节点。 - 如果
type
曾经是个vnode
,则拷贝一个新的vnode
返回。 - 解决
class component
- 兼容
vue2
的异步组件及函数式组件 class
及style
的标准化- 依据
type
属性初步确定patchFlag
- 调用
createBaseVNode
办法创立vnode
并返回
createBaseVNode
:
- 先创立一个
vnode
对象 - 欠缺
children
及patchFlag
属性 - 判断是否应该被父
Block
收集 - 解决兼容
vue2
- 返回
vnode