詳解Vue3中對VDOM的改進
前言
vue-next 對virtual dom的patch更新做了一系列的優(yōu)化,從編譯時加入了 block 以減少 vdom 之間的對比次數(shù),另外還有 hoisted 的操作減少了內(nèi)存的開銷。本文寫給自己看,做個知識點記錄,如有錯誤,還請不吝賜教。
VDOM
VDOM的概念簡單來說就是用js對象來模擬真實DOM樹。由于MV**的架構(gòu),真實DOM樹應(yīng)該隨著數(shù)據(jù)(Vue2.x中的data)的改變而發(fā)生改變,這些改變可能是以下幾個方面:
- v-if
- v-for
- 動態(tài)的props(如:class,@click)
- 子節(jié)點的改變
- 等等
Vue框架要做的其實很單一:在用戶改變數(shù)據(jù)時,正確更新DOM樹,做法就是其核心的VDOM的patch和diff算法。
Vue2.x中的做法
在Vue2.x中,當數(shù)據(jù)改變后就要對所有的節(jié)點進行patch和diff操作。如以下DOM結(jié)構(gòu):
<div>
<span class="header">I'm header</span>
<ul>
<li>第一個靜態(tài)li</li>
<li v-for="item in mutableItems" :key="item.key"> {{ item.desc }}</li>
</ul>
</div>
在第一次mount節(jié)點的時候會去生成真實的DOM,此后如果
mutableItems.push({
key: 'asdf',
desc: 'a new li item'
})
預(yù)期的結(jié)果是頁面出現(xiàn)新的一個li元素,內(nèi)容就是 a new li item,Vue2.x中是通過patch時對 ul 元素對應(yīng)的 vnode 的 children 來進行 diff 操作,具體操作在此不深究,但是該操作是需要比較所有的 li 對應(yīng)的 vnode 的。
不足
正是由于2.x版本中的diff操作需要遍歷所有元素,本例中包括了 span 和 第一個li元素,但是這兩個元素是靜態(tài)的,不需要被比較的,不論數(shù)據(jù)怎么變,靜態(tài)元素都不會再更改了。vue-next在編譯時對這種操作做了優(yōu)化,即 Block。
Block
入上述模板,在vue-next中生成的渲染函數(shù)為:
const _Vue = Vue
const { createVNode: _createVNode } = _Vue
const _hoisted_1 = _createVNode("span", { class: "header" }, "I'm header", -1 /* HOISTED */)
const _hoisted_2 = _createVNode("li", null, "第一個靜態(tài)li", -1 /* HOISTED */)
return function render(_ctx, _cache) {
with (_ctx) {
const { createVNode: _createVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock, toDisplayString: _toDisplayString } = _Vue
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("ul", null, [
_hoisted_2,
(_openBlock(true), _createBlock(_Fragment, null, _renderList(state.mutableItems, (item) => {
return (_openBlock(), _createBlock("li", { key: item.key }, _toDisplayString(item.desc), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */))
])
], 64 /* STABLE_FRAGMENT */))
}
}
我們可以看到調(diào)用了 openBlock 和 createBlock 方法,這兩個方法的代碼實現(xiàn)也很簡單:
const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
let shouldTrack = 1
// openBlock
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
export function createBlock(
type: VNodeTypes | ClassComponent,
props?: { [key: string]: any } | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[]
): VNode {
// avoid a block with patchFlag tracking itself
shouldTrack--
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
if (currentBlock) {
currentBlock.push(vnode)
}
return vnode
}
更加詳細的注釋還請看源代碼中的注釋,寫的十分詳盡,便于理解。這里面 openBlock 就是初始化一個塊,createBlock 就是對當前編譯的內(nèi)容生成一個塊,這里面的這一行代碼:vnode.dynamicChildren = currentBlock || EMPTY_ARR 就是在收集動態(tài)的子節(jié)點,我們可以再看一下編譯時運行的函數(shù):
// createVNode
function _createVNode(
type: VNodeTypes | ClassComponent,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null
) {
/**
* 一系列代碼
**/
// 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 &&
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.STATEFUL_COMPONENT ||
shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT)
) {
currentBlock.push(vnode)
}
}
上述函數(shù)是在模板編譯成ast之后調(diào)用的生成VNode的函數(shù),所以有patchFlag這個標志,如果是動態(tài)的節(jié)點,并且此時是開啟了Block的話,就會將節(jié)點塞入Block中,這樣 createBlock返回的 VNode 中就會有 dynamicChildren 了。
到此為止,通過本文中案例經(jīng)過模板編譯和render函數(shù)運行后并經(jīng)過了優(yōu)化以后生成了如下結(jié)構(gòu)的vnode:
const result = {
type: Symbol(Fragment),
patchFlag: 64,
children: [
{ type: 'span', patchFlag: -1, ...},
{
type: 'ul',
patchFlag: 0,
children: [
{ type: 'li', patchFlag: -1, ...},
{
type: Symbol(Fragment),
children: [
{ type: 'li', patchFlag: 1 ...},
{ type: 'li', patchFlag: 1 ...}
]
}
]
}
],
dynamicChildren: [
{
type: Symbol(Fragment),
patchFlag: 128,
children: [
{ type: 'li', patchFlag: 1 ...},
{ type: 'li', patchFlag: 1 ...}
]
}
]
}
以上的 result 不完整,但是我們暫時只關(guān)心這些屬性??梢钥匆?result.children 的第一個元素是span,patchFlag=-1,且 result 有一個 dynamicChildren 數(shù)組,里面只包含了兩個動態(tài)的 li,后續(xù)如果變動了數(shù)據(jù),那么新的 vnode.dynamicChildren 會有第三個 li 元素。
patch
patch部分其實也沒差多少,就是根據(jù)vnode的type執(zhí)行不同的patch操作:
function patchElement(n1, n2) {
let { dynamicChildren } = n2
// 一系列操作
if (dynamicChildren) {
patchBlockChildren (
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG
)
} else if (!optimized) {
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG
)
}
}
可以看見,如果有了 dynamicChildren 那么vue2.x版本中的diff操作就被替換成了 patchBlockChildren() 且參數(shù)只有 dynamicChildren,就是靜態(tài)的不做diff操作了,而如果vue-next的patch中沒有 dynamicChildren,則進行完整的diff操作,入注釋寫的 full diff 的后續(xù)代碼。
結(jié)尾
本文沒有深入講解代碼的實現(xiàn)層面,一是因為自己實力不濟還在閱讀源碼當中,二是我個人認為閱讀源碼不可鉆牛角尖,從大局入眼,再徐徐圖之,先明白了各個部分的作用后帶著思考去閱讀源碼能收獲到的應(yīng)該更多一些。
到此這篇關(guān)于詳解Vue3中對VDOM的改進的文章就介紹到這了,更多相關(guān)Vue3 VDOM內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue自定義指令添加跟隨鼠標光標提示框v-tooltip方式
這篇文章主要介紹了vue自定義指令添加跟隨鼠標光標提示框v-tooltip方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
淺談vue.js導(dǎo)入css庫(elementUi)的方法
下面小編就為大家分享一篇淺談vue.js導(dǎo)入css庫(elementUi)的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03

