這篇文章主要為大家展示了Vue3中如何改進(jìn)VDOM,內(nèi)容簡而易懂,希望大家可以學(xué)習(xí)一下,學(xué)習(xí)完之后肯定會有收獲的,下面讓小編帶大家一起來看看吧。
成都創(chuàng)新互聯(lián)公司專注于企業(yè)成都全網(wǎng)營銷、網(wǎng)站重做改版、郯城網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5頁面制作、成都做商城網(wǎng)站、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為郯城等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。
前言
vue-next 對virtual dom的patch更新做了一系列的優(yōu)化,從編譯時(shí)加入了 block 以減少 vdom 之間的對比次數(shù),另外還有 hoisted 的操作減少了內(nèi)存的開銷。本文寫給自己看,做個(gè)知識點(diǎn)記錄,如有錯(cuò)誤,還請不吝賜教。
VDOM
VDOM的概念簡單來說就是用js對象來模擬真實(shí)DOM樹。由于MV**的架構(gòu),真實(shí)DOM樹應(yīng)該隨著數(shù)據(jù)(Vue2.x中的data)的改變而發(fā)生改變,這些改變可能是以下幾個(gè)方面:
Vue框架要做的其實(shí)很單一:在用戶改變數(shù)據(jù)時(shí),正確更新DOM樹,做法就是其核心的VDOM的patch和diff算法。
Vue2.x中的做法
在Vue2.x中,當(dāng)數(shù)據(jù)改變后就要對所有的節(jié)點(diǎn)進(jìn)行patch和diff操作。如以下DOM結(jié)構(gòu):
I'm header
- 第一個(gè)靜態(tài)li
- {{ item.desc }}
在第一次mount節(jié)點(diǎn)的時(shí)候會去生成真實(shí)的DOM,此后如果
mutableItems.push({ key: 'asdf', desc: 'a new li item' })
預(yù)期的結(jié)果是頁面出現(xiàn)新的一個(gè)li元素,內(nèi)容就是 a new li item,Vue2.x中是通過patch時(shí)對 ul 元素對應(yīng)的 vnode 的 children 來進(jìn)行 diff 操作,具體操作在此不深究,但是該操作是需要比較所有的 li 對應(yīng)的 vnode 的。
不足
正是由于2.x版本中的diff操作需要遍歷所有元素,本例中包括了 span 和 第一個(gè)li元素,但是這兩個(gè)元素是靜態(tài)的,不需要被比較的,不論數(shù)據(jù)怎么變,靜態(tài)元素都不會再更改了。vue-next在編譯時(shí)對這種操作做了優(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, "第一個(gè)靜態(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 方法,這兩個(gè)方法的代碼實(shí)現(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 }
更加詳細(xì)的注釋還請看源代碼中的注釋,寫的十分詳盡,便于理解。這里面 openBlock 就是初始化一個(gè)塊,createBlock 就是對當(dāng)前編譯的內(nèi)容生成一個(gè)塊,這里面的這一行代碼:vnode.dynamicChildren = currentBlock || EMPTY_ARR 就是在收集動態(tài)的子節(jié)點(diǎn),我們可以再看一下編譯時(shí)運(yùn)行的函數(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這個(gè)標(biāo)志,如果是動態(tài)的節(jié)點(diǎn),并且此時(shí)是開啟了Block的話,就會將節(jié)點(diǎn)塞入Block中,這樣 createBlock返回的 VNode 中就會有 dynamicChildren 了。
到此為止,通過本文中案例經(jīng)過模板編譯和render函數(shù)運(yùn)行后并經(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 不完整,但是我們暫時(shí)只關(guān)心這些屬性??梢钥匆?result.children 的第一個(gè)元素是span,patchFlag=-1,且 result 有一個(gè) dynamicChildren 數(shù)組,里面只包含了兩個(gè)動態(tài)的 li,后續(xù)如果變動了數(shù)據(jù),那么新的 vnode.dynamicChildren 會有第三個(gè) li 元素。
patch
patch部分其實(shí)也沒差多少,就是根據(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,則進(jìn)行完整的diff操作,入注釋寫的 full diff 的后續(xù)代碼。
以上就是關(guān)于Vue3中如何改進(jìn)VDOM的內(nèi)容,如果你們有學(xué)習(xí)到知識或者技能,可以把它分享出去讓更多的人看到。