【vue3源碼】十二、認識虛擬DOM
什么是虛擬DOM撕攒?
虛擬DOM(也可以稱為vnode
)描述了一個真實的DOM結(jié)構(gòu)陡鹃,它和真實DOM一樣都是由很多節(jié)點組成的一個樹形結(jié)構(gòu)。本質(zhì)其實就是一個JS對象抖坪,如下就是一個vnode
:
{
type: 'div',
props: {
id: 'container'
},
children: [
{
type: 'span',
props: {
class: 'span1'
},
children: 'Hello '
},
{
type: 'span',
props: {
class: 'span2'
},
children: 'World'
},
]
}
上面這個vnode
描述的真實DOM結(jié)構(gòu)如下:
<div id="container">
<span class="text1">Hello </span>
<span class="text2">World</span>
</div>
可以發(fā)現(xiàn)萍鲸,虛擬節(jié)點的type
描述了標簽的類型,props
描述了標簽的屬性擦俐,children
描述了標簽的子節(jié)點脊阴。當(dāng)然一個vnode
不僅只有這三個屬性。vue3
中對vnode
的類型定義如下:
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
// 標記為一個VNode
__v_isVNode: true
// 禁止將VNode處理為響應(yīng)式對象
[ReactiveFlags.SKIP]: true
// 節(jié)點類型
type: VNodeTypes
// 節(jié)點的屬性
props: (VNodeProps & ExtraProps) | null
// 便與DOM的復(fù)用蚯瞧,主要用在diff算法中
key: string | number | symbol | null
// 被用來給元素或子組件注冊引用信息
ref: VNodeNormalizedRef | null
scopeId: string | null
slotScopeIds: string[] | null
// 子節(jié)點
children: VNodeNormalizedChildren
// 組件實例
component: ComponentInternalInstance | null
// 指令信息
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
// vnode對應(yīng)的DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
// teleport需要掛載的目標DOM
target: HostElement | null
// teleport掛載所需的錨點
targetAnchor: HostNode | null
// 對于Static vnode所包含的靜態(tài)節(jié)點數(shù)量
staticCount: number
// suspense組件的邊界
suspense: SuspenseBoundary | null
// suspense的default slot對應(yīng)的vnode
ssContent: VNode | null
// suspense的fallback slot對應(yīng)的vnode
ssFallback: VNode | null
// 用于優(yōu)化的標記嘿期,主要用于判斷節(jié)點類型
shapeFlag: number
// 用于diff優(yōu)化的補丁標記
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
}
如何創(chuàng)建虛擬DOM
vue3
對外提供了h()
方法用于創(chuàng)建虛擬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是對象且不是數(shù)組
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// propsOrChildren是vnode
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// 有props無子節(jié)點
return createVNode(type, propsOrChildren)
} else {
// 有子節(jié)點
return createVNode(type, null, propsOrChildren)
}
} else {
// 如果參數(shù)大于3埋合,那么第三個參數(shù)及之后的參數(shù)都會被作為子節(jié)點處理
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
在h
函數(shù)會使用createVNode
函數(shù)創(chuàng)建虛擬DOM备徐。
export const createVNode = (
__DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _createVNode
可以看到createVNode
在開發(fā)環(huán)境下會使用createVNodeWithArgsTransform
,其他環(huán)境下會使用_createVNode
饥悴。這里我們只看下_createVNode
的實現(xiàn)坦喘。
<details>
<summary><code>_createVNode</code>完整代碼</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已經(jīng)是個vnode,則復(fù)制個新的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的異步及函數(shù)式組件
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)
}
}
// 根據(jù)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個參數(shù):
-
type
:vnode
類型 -
props
:vnode
的屬性 -
children
:子vnode
-
patchFlag
:補丁標記瓣铣,由編譯器生成vnode
時的優(yōu)化提示,在diff期間會進入對應(yīng)優(yōu)化 -
dynamicProps
:動態(tài)屬性 -
isBlockNode
:是否是個Block
節(jié)點
首先會對type
進行校驗贷揽,如果type
是空的動態(tài)組件棠笑,進行提示,并將type
指定為一個Comment
注釋DOM禽绪。
if (!type || type === NULL_DYNAMIC_COMPONENT) {
if (__DEV__ && !type) {
warn(`Invalid vnode type when creating vnode: ${type}.`)
}
type = Comment
}
如果type
已經(jīng)是個vnode
蓖救,會從type
復(fù)制出一個新的vnode
洪规。這種情況主要在<component :is="vnode"/>
情況下發(fā)生
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
}
關(guān)于cloneVNode
的實現(xiàn):
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循捺,則設(shè)置為null
key: mergedProps && normalizeKey(mergedProps),
// 如果過存在額外的ref
// 如果過需要合并ref
// 如果被拷貝節(jié)點中的ref是個數(shù)組斩例,將調(diào)用normalizeRef處理ref,并將結(jié)果合并到被拷貝節(jié)點中的ref中
// 否則从橘,創(chuàng)建一個新的數(shù)組念赶,存儲ref和normalizeRef(extraProps)的結(jié)果
// 否則直接調(diào)用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 克隆,我們不能再假設(shè)其現(xiàn)有的補丁標志是可靠的恰力,需要添加 FULL_PROPS 標志
// 如果存在extraProps叉谜,并且vnode.type不是是Fragment片段的情況下:
// 如果patchFlag為-1,說明是靜態(tài)節(jié)點踩萎,它的內(nèi)容不會發(fā)生變化停局。新的vnode的patchFlag為PatchFlags.FULL_PROPS,表示props中存在動態(tài)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
}
在復(fù)制節(jié)點的過程中主要處理經(jīng)歷以下步驟:
- 被拷貝節(jié)點的
props
與額外的props
的合并 - 創(chuàng)建新的
vnode
-
key
的處理:取合并后的props
中的key
董栽,如果不存在,取null
-
ref
的合并:根據(jù)是否需要合并ref
企孩,決定是否合并ref
-
patchFlag
的處理:如果vnode
使用額外的props
克隆裆泳,補丁標志不再可靠的,需要添加FULL_PROPS
標志 -
ssContent
的處理:使用cloneVNode
復(fù)制被拷貝節(jié)點的ssContent
-
ssFallback
的處理:使用cloneVNode
復(fù)制被拷貝節(jié)點的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) {
// 建立一個數(shù)組并調(diào)用normalizeClass工禾,最終class會是字符串的形式
ret.class = normalizeClass([ret.class, toMerge.class])
}
} else if (key === 'style') {
// 建立style數(shù)組并調(diào)用normalizeStyle,最終style是對象形式
ret.style = normalizeStyle([ret.style, toMerge.style])
} else if (isOn(key)) { // 以on開頭的屬性蝗柔,統(tǒng)一按事件處理
const existing = ret[key]
const incoming = toMerge[key]
// 如果已經(jīng)存在的key對應(yīng)事件與incoming不同闻葵,并且已經(jīng)存在的key對應(yīng)事件中不包含incoming
if (
incoming &&
existing !== incoming &&
!(isArray(existing) && existing.includes(incoming))
) {
// 如果過存在existing,將existing癣丧、incoming合并到一個新的數(shù)組中
ret[key] = existing
? [].concat(existing as any, incoming as any)
: incoming
}
} else if (key !== '') { // 其他情況直接對ret[key]進行賦值槽畔,靠后合并的值會取代之前的值
ret[key] = toMerge[key]
}
}
}
return ret
}
關(guān)于normalizeClass
、normalizeStyle
的實現(xiàn):
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
中胁编,當(dāng)復(fù)制出一個新的vnode
后厢钧,調(diào)用了一個normalizeChildren
方法,該方法的作用是對新復(fù)制的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數(shù)數(shù)組,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)
// 將默認插槽的結(jié)果作為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!)) {
// 如果槽未規(guī)范化霞扬,則附加上下文實例(編譯過或標準話的slots已經(jīng)有上下文)
;(children as RawSlots)._ctx = currentRenderingInstance
} else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
// 子組件接收來自父組件的轉(zhuǎn)發(fā)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
是否應(yīng)該被收集到Block
中喻圃,并返回拷貝的節(jié)點萤彩。
如果type
不是vnode
,在方法最后會調(diào)用一個createBaseVNode
創(chuàng)建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的存在表明該節(jié)點需要修補更新斧拍。
// 組件節(jié)點也應(yīng)該總是打補丁雀扶,因為即使組件不需要更新,它也需要將實例持久化到下一個 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
}
總結(jié)
虛擬DOM
的創(chuàng)建流程:
- 如果
type
是個空的動態(tài)組件怕吴,將vnode.type
指定為Comment
注釋節(jié)點。 - 如果
type
已經(jīng)是個vnode
县踢,則拷貝一個新的vnode
返回。 - 處理
class component
- 兼容
vue2
的異步組件及函數(shù)式組件 -
class
及style
的標準化 - 根據(jù)
type
屬性初步確定patchFlag
- 調(diào)用
createBaseVNode
方法創(chuàng)建vnode
并返回
createBaseVNode
:
- 先創(chuàng)建一個
vnode
對象 - 完善
children
及patchFlag
屬性 - 判斷是否應(yīng)該被父
Block
收集 - 處理兼容
vue2
- 返回
vnode