【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 = /;(?![^(]*\))/gconst 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