探索Virtual DOM的前世今生,virtualdom

在前端開(kāi)發(fā)過(guò)程中,對(duì)性能產(chǎn)生最大影響的因素莫過(guò)于DOM的重排重繪了,React作為前端框架領(lǐng)跑者,為了有效解決DOM更新開(kāi)銷(xiāo)的問(wèn)題,采用了Virtual DOM的思路,不僅提升了DOM操作的效率,更推動(dòng)了數(shù)據(jù)驅(qū)動(dòng)式組件開(kāi)發(fā)的形成與完善。愛(ài)掏網(wǎng) - it200.com一旦習(xí)慣了數(shù)據(jù)驅(qū)動(dòng)式開(kāi)發(fā),再要求我們使用顯式DOM操作開(kāi)發(fā)的話,虐心程度無(wú)異于春節(jié)返鄉(xiāng)的車(chē)票賣(mài)完了,只能坐長(zhǎng)途輾轉(zhuǎn)煎熬了。愛(ài)掏網(wǎng) - it200.com

1

而VirtualDOM的主要思想就是模擬DOM的樹(shù)狀結(jié)構(gòu),在內(nèi)存中創(chuàng)建保存映射DOM信息的節(jié)點(diǎn)數(shù)據(jù),在由于交互等因素需要視圖更新時(shí),先通過(guò)對(duì)節(jié)點(diǎn)數(shù)據(jù)進(jìn)行diff后得到差異結(jié)果后,再一次性對(duì)DOM進(jìn)行批量更新操作,這就好比在內(nèi)存中創(chuàng)建了一個(gè)平行世界,瀏覽器中DOM樹(shù)的每一個(gè)節(jié)點(diǎn)與屬性數(shù)據(jù)都在這個(gè)平行世界中存在著另一個(gè)版本的虛擬DOM樹(shù),所有復(fù)雜曲折的更新邏輯都在平行世界中的VirtualDOM處理完成,只將最終的更新結(jié)果發(fā)送給瀏覽器中的DOM樹(shù)執(zhí)行,這樣就避免了冗余瑣碎的DOM樹(shù)操作負(fù)擔(dān),進(jìn)而有效提高了性能。愛(ài)掏網(wǎng) - it200.com

如果你已經(jīng)是熟練使用vue或者react的項(xiàng)目老手,本文將助你一探這些前端框架進(jìn)行視圖更新背后的工作原理,并且可以一定程度掌握VirtualDOM的核心算法,即便你還未享受過(guò)這些數(shù)據(jù)驅(qū)動(dòng)的工具帶來(lái)的便利,通過(guò)閱讀本文,你也將了解到一些當(dāng)下的前端框架是如何對(duì)開(kāi)發(fā)模式產(chǎn)生巨變影響的。愛(ài)掏網(wǎng) - it200.com同時(shí)本文也是我們對(duì)相關(guān)知識(shí)學(xué)習(xí)的一個(gè)總結(jié),難免有誤,歡迎多多指正,并期待大大們的指點(diǎn)。愛(ài)掏網(wǎng) - it200.com


VirtualDOM是react在組件化開(kāi)發(fā)場(chǎng)景下,針對(duì)DOM重排重繪性能瓶頸作出的重要優(yōu)化方案,而他最具價(jià)值的核心功能是如何識(shí)別并保存新舊節(jié)點(diǎn)數(shù)據(jù)結(jié)構(gòu)之間差異的方法,也即是diff算法愛(ài)掏網(wǎng) - it200.com毫無(wú)疑問(wèn)的是diff算法的復(fù)雜度與效率是決定VirtualDOM能夠帶來(lái)性能提升效果的關(guān)鍵因素。愛(ài)掏網(wǎng) - it200.com因此,在VirtualDOM方案被提出之后,社區(qū)中不斷涌現(xiàn)出對(duì)diff的改進(jìn)算法,引用司徒正美的經(jīng)典介紹:

最開(kāi)始經(jīng)典的深度優(yōu)先遍歷DFS算法,其復(fù)雜度為O(n^3),存在高昂的diff成本,然后是cito.js的橫空出世,它對(duì)今后所有虛擬DOM的算法都有重大影響。愛(ài)掏網(wǎng) - it200.com它采用兩端同時(shí)進(jìn)行比較的算法,將diff速度拉高到幾個(gè)層次。愛(ài)掏網(wǎng) - it200.com緊隨其后的是kivi.js,在cito.js的基出提出兩項(xiàng)優(yōu)化方案,使用key實(shí)現(xiàn)移動(dòng)追蹤及基于key的編輯長(zhǎng)度距離算法應(yīng)用(算法復(fù)雜度 為O(n^2))。愛(ài)掏網(wǎng) - it200.com但這樣的diff算法太過(guò)復(fù)雜了,于是后來(lái)者snabbdom將kivi.js進(jìn)行簡(jiǎn)化,去掉編輯長(zhǎng)度距離算法,調(diào)整兩端比較算法。愛(ài)掏網(wǎng) - it200.com速度略有損失,但可讀性大大提高。愛(ài)掏網(wǎng) - it200.com再之后,就是著名的vue2.0 把snabbdom整個(gè)庫(kù)整合掉了。愛(ài)掏網(wǎng) - it200.com

因此目前VirtualDOM的主流diff算法趨向一致,在主要diff思路上,snabbdom與react的reconilation方式基本相同。愛(ài)掏網(wǎng) - it200.com

  • 按tree層級(jí)diff(level by level)

由于diff的數(shù)據(jù)結(jié)構(gòu)是以DOM渲染為目標(biāo)的模擬樹(shù)狀層級(jí)結(jié)構(gòu)的節(jié)點(diǎn)數(shù)據(jù),而在WebUI中很少出現(xiàn)DOM的層級(jí)結(jié)構(gòu)因?yàn)榻换ザa(chǎn)生更新,因此VirtualDOM的diff策略是在新舊節(jié)點(diǎn)樹(shù)之間按層級(jí)進(jìn)行diff得到差異,而非傳統(tǒng)的按深度遍歷搜索,這種通過(guò)大膽假設(shè)得到的改進(jìn)方案,不僅符合實(shí)際場(chǎng)景的需要,而且大幅降低了算法實(shí)現(xiàn)復(fù)雜度,從O(n^3)提升至O(n)。愛(ài)掏網(wǎng) - it200.com


  • 按類(lèi)型進(jìn)行diff

無(wú)論VirtualDOM中的節(jié)點(diǎn)數(shù)據(jù)對(duì)應(yīng)的是一個(gè)原生的DOM節(jié)點(diǎn)還是vue或者react中的一個(gè)組件,不同類(lèi)型的節(jié)點(diǎn)所具有的子樹(shù)節(jié)點(diǎn)之間結(jié)構(gòu)往往差異明顯,因此對(duì)不同類(lèi)型的節(jié)點(diǎn)的子樹(shù)進(jìn)行diff的投入成本與產(chǎn)出比將會(huì)很高昂,為了提升diff效率,VirtualDOM只對(duì)相同類(lèi)型的同一個(gè)節(jié)點(diǎn)進(jìn)行diff,當(dāng)新舊節(jié)點(diǎn)發(fā)生了類(lèi)型的改變時(shí),則并不進(jìn)行子樹(shù)的比較,直接創(chuàng)建新類(lèi)型的VirtualDOM,替換舊節(jié)點(diǎn)。愛(ài)掏網(wǎng) - it200.com


  • 列表diff

當(dāng)被diff節(jié)點(diǎn)處于同一層級(jí)時(shí),通過(guò)三種節(jié)點(diǎn)操作新舊節(jié)點(diǎn)進(jìn)行更新:插入,移動(dòng)和刪除,同時(shí)提供給用戶(hù)設(shè)置key屬性的方式調(diào)整diff更新中默認(rèn)的排序方式,在沒(méi)有key值的列表diff中,只能通過(guò)按順序進(jìn)行每個(gè)元素的對(duì)比,更新,插入與刪除,在數(shù)據(jù)量較大的情況下,diff效率低下,如果能夠基于設(shè)置key標(biāo)識(shí)盡心diff,就能夠快速識(shí)別新舊列表之間的變化內(nèi)容,提升diff效率。愛(ài)掏網(wǎng) - it200.com


基于以上的三條diff原則,我們就可以自由選擇Virtual DOM的具體方案,甚至自己動(dòng)手進(jìn)行diff實(shí)踐,在那之前,讓我們先以Vue中的snabbdom與React中的Reconcile這兩個(gè)Virtual DOM的實(shí)現(xiàn)方案為對(duì)象進(jìn)行學(xué)習(xí)。愛(ài)掏網(wǎng) - it200.com

在眾多VirtuaDOM實(shí)現(xiàn)方案中,snabbdom以其高效的實(shí)現(xiàn),小巧的體積與靈活的可擴(kuò)展性脫穎而出,它的核心代碼只有300行+,卻已被適用于vue等輕量級(jí)前端框架中作為VirtualDOM的主要功能實(shí)現(xiàn)。愛(ài)掏網(wǎng) - it200.com

一個(gè)使用snabbdom創(chuàng)建的demo是這樣的:

import snabbdom from 'snabbdom';
import h from 'snabbdom/h';
const patch = snabbdom.init([
  require('snabbdom/modules/class'),          // makes it easy to toggle classes
  require('snabbdom/modules/props'),          // for setting properties on DOM elements
  require('snabbdom/modules/style'),          // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners'), // attaches event listeners
]);

var vnode = h('div', {style: {fontWeight: 'bold'}}, 'Hello world');
patch(document.getElementById('placeholder'), vnode)
復(fù)制代碼

在snabbdom中提供了h函數(shù)做為創(chuàng)建VirtualDOM的主要函數(shù),h函數(shù)接受的三個(gè)參數(shù)同時(shí)揭示了diff算法中關(guān)注的三個(gè)核心:節(jié)點(diǎn)類(lèi)型,屬性數(shù)據(jù),子節(jié)點(diǎn)對(duì)象。愛(ài)掏網(wǎng) - it200.com而patch方法即是用來(lái)創(chuàng)建初始DOM節(jié)點(diǎn)與更新VirtualDOM的diff核心函數(shù)。愛(ài)掏網(wǎng) - it200.com

function view(name) { 
  return h('div', [
    h('input', {
      props: { type: 'text', placeholder: 'Type a your name' },
      on   : { input: update }
    }),
    h('hr'),
    h('div', 'Hello ' + name)
  ]); 
}

var oldVnode = document.getElementById('placeholder');

function update(event) {
  const newVnode = view(event.target.value);
  oldVnode = patch(oldVnode, newVnode);
}

oldVnode = patch(oldVnode, view(''));
復(fù)制代碼

以上是一個(gè)通過(guò)input事件觸發(fā)VirtualDOM更新的典型app。愛(ài)掏網(wǎng) - it200.com在h函數(shù)中,不光可以為VirtualDOM保存數(shù)據(jù)屬性,還可以設(shè)置事件回調(diào)函數(shù),并在其中獲取并處理相關(guān)的事件屬性,如update回調(diào)中的event對(duì)象。愛(ài)掏網(wǎng) - it200.com通過(guò)捕獲事件中創(chuàng)建新的vnode,與舊的vnode進(jìn)行diff,最終對(duì)當(dāng)前的oldVnode進(jìn)行更新,并向用戶(hù)展示更新結(jié)果,他的工作流程如下:


在snabbdom源碼中的核心patch函數(shù)中很明顯的體現(xiàn)了VirtualDOM的按類(lèi)型diff與列表diff的策略:如果patch的新舊節(jié)點(diǎn)經(jīng)過(guò)sameVnode判斷不是同一個(gè)節(jié)點(diǎn),則進(jìn)行新節(jié)點(diǎn)的創(chuàng)建插入與舊節(jié)點(diǎn)的刪除,而sameVnode也即是判斷兩個(gè)節(jié)點(diǎn)是否有相同的key標(biāo)識(shí)與傳入的帶有節(jié)點(diǎn)類(lèi)型等信息的selector字符串是否相同為依據(jù)的:

function sameVnode(vnode1, vnode2) {
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}
復(fù)制代碼

而對(duì)相同節(jié)點(diǎn)進(jìn)行新舊diff的主函數(shù)patchVnode的實(shí)現(xiàn)流程如下,其中oldCh與ch為保存舊的當(dāng)前節(jié)點(diǎn)與將要更新的新節(jié)點(diǎn):


//新的text不存在
        if (isUndef(vnode.text)) {
            if (isDef(oldCh) && isDef(ch)) {
                if (oldCh !== ch)
                    updateChildren(elm, oldCh, ch, insertedVnodeQueue);
            }
            //舊的子節(jié)點(diǎn)不存在,新的存在
            else if (isDef(ch)) {
                //舊的text存在
                if (isDef(oldVnode.text))
                    api.setTextContent(elm, '');
                //把新的插入到elm底下
                addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
            }
            //新的子節(jié)點(diǎn)不存在,舊的存在
            else if (isDef(oldCh)) {
                removeVnodes(elm, oldCh, 0, oldCh.length - 1);
            }
            //新的子節(jié)點(diǎn)不存在,舊的text存在
            else if (isDef(oldVnode.text)) {
                api.setTextContent(elm, '');
            }
       
復(fù)制代碼
  1. 如果新節(jié)點(diǎn)可能是復(fù)雜節(jié)點(diǎn)而非text節(jié)點(diǎn),則對(duì)節(jié)點(diǎn)的children進(jìn)一步diff:先判斷是否存在新節(jié)點(diǎn)的children整體新增或刪除的情況,若是則進(jìn)行批量更新, 而新舊節(jié)點(diǎn)都包含children列表的情況進(jìn)行updateChildren處理
  2. 如果新舊節(jié)點(diǎn)都是text節(jié)點(diǎn),且兩者不同則只進(jìn)行text更新即可

以下介紹updateChildren的核心diff方式,以舊節(jié)點(diǎn)oldCh為當(dāng)前VirtualDOM狀態(tài),將新節(jié)點(diǎn)newCh的變化對(duì)oldCh進(jìn)行更新得到新的VirtualDOM狀態(tài),并記錄新舊節(jié)點(diǎn)的startIndex與endIndex兩端同時(shí)比較,這樣會(huì)比從單向按順序?qū)Ρ鹊姆绞礁斓玫絛iff結(jié)果:

  • 當(dāng)新舊節(jié)點(diǎn)的startVnode與endVnode 各自對(duì)應(yīng)相同時(shí),繼續(xù)對(duì)比,startVnode與endVnode位置各自向中間移動(dòng)一位。愛(ài)掏網(wǎng) - it200.com
  • 發(fā)現(xiàn)oldStartVnode,newEndVnode相同時(shí),也就是oldStartVnode成為了新的末端節(jié)點(diǎn),就將oldStartVnode插到oldEndVnode的后一個(gè)位置



  • 當(dāng)oldEndVnode,newStartVnode相同時(shí),也就是oldEndVnode成為了新的頭部節(jié)點(diǎn),就將oldEndVnode插入到oldStartVnode前一個(gè)位置



  • 當(dāng)發(fā)現(xiàn)oldCh里沒(méi)有當(dāng)前newCh中的節(jié)點(diǎn),將新節(jié)點(diǎn)插入到oldStartVnode的前邊,同時(shí)這里會(huì)借助節(jié)點(diǎn)中的key值進(jìn)行map查找是否在其他位置中有匹配的舊節(jié)點(diǎn),如果有匹配,就對(duì)舊節(jié)點(diǎn)進(jìn)行更新,再將其插入到當(dāng)前的oldStartVnode的前面。愛(ài)掏網(wǎng) - it200.com


1
  • 在這一輪對(duì)比結(jié)束時(shí)后,有兩種情況,當(dāng)oldStartIdx > oldEndIdx,說(shuō)明舊節(jié)點(diǎn)oldCh已經(jīng)遍歷完。愛(ài)掏網(wǎng) - it200.com那么剩下newStartIdx和newEndIdx之間的vnode的新節(jié)點(diǎn)就調(diào)用addVnodes,批量插入父節(jié)點(diǎn)的before節(jié)點(diǎn)位置,before很多時(shí)候是為null的。愛(ài)掏網(wǎng) - it200.comaddVnodes調(diào)用的是insertBefore操作dom節(jié)點(diǎn),我們看看insertBefore的文檔:parentElement.insertBefore(newElement, referenceElement)如果referenceElement為null則newElement將被插入到子節(jié)點(diǎn)的末尾。愛(ài)掏網(wǎng) - it200.com如果newElement已經(jīng)在DOM樹(shù)中,newElement首先會(huì)從DOM樹(shù)中移除。愛(ài)掏網(wǎng) - it200.com所以before為null,newElement將被插入到子節(jié)點(diǎn)的末尾。愛(ài)掏網(wǎng) - it200.com
  • 如果newStartIdx > newEndIdx,就是newCh先在第一輪對(duì)比中遍歷完。愛(ài)掏網(wǎng) - it200.com此時(shí)oldCh中的oldStartIdx和oldEndIdx之間的vnode是需要被刪除的,調(diào)用removeVnodes將它們從dom里刪除。愛(ài)掏網(wǎng) - it200.com


在react的歷史版本中,完成數(shù)據(jù)節(jié)點(diǎn)diff的過(guò)程是reconcilation,,當(dāng)你在一個(gè)組件中調(diào)用setState時(shí),react會(huì)將該組件節(jié)點(diǎn)標(biāo)記為dirty,進(jìn)行reconcile并得到重新構(gòu)建的子樹(shù)virtual-dom,在工作流結(jié)束時(shí)重新render帶有dirty標(biāo)記的節(jié)點(diǎn), 如果你是在組件的根節(jié)點(diǎn)上進(jìn)行setState,那么整個(gè)組件樹(shù)Virtual DOM都會(huì)重新創(chuàng)建,但由于這并不是直接操作真實(shí)的DOM,所以實(shí)際上產(chǎn)生的影響仍然有限。愛(ài)掏網(wǎng) - it200.com

在React16的重寫(xiě)中,最重要的改變時(shí)將核心架構(gòu)改為了代號(hào)為Fiber的異步渲染架構(gòu)。愛(ài)掏網(wǎng) - it200.com從本質(zhì)上看,一個(gè)Fiber就是一個(gè)POJO對(duì)象,一個(gè)React Element可以對(duì)應(yīng)一個(gè)或多個(gè)Fiber節(jié)點(diǎn),F(xiàn)iber包含著DOM節(jié)點(diǎn)與React組件中的所有工作需要的屬性數(shù)據(jù)。愛(ài)掏網(wǎng) - it200.com因此雖然React的代碼中其實(shí)沒(méi)有明確的Virtual DOM概念,但通過(guò)對(duì)Fiber的設(shè)計(jì)充分完成了Virtual DOM的功能與機(jī)制。愛(ài)掏網(wǎng) - it200.com

Fiber除了承擔(dān)Virtual DOM的工作之外,它真正設(shè)計(jì)目的是實(shí)現(xiàn)一種在前端執(zhí)行的輕量執(zhí)行線程,同普通線程一樣共享定址空間,但卻能夠受React自身的Fiber系統(tǒng)調(diào)度,實(shí)現(xiàn)渲染任務(wù)細(xì)分,可計(jì)時(shí),可打斷,可重啟,可調(diào)度的協(xié)作式多任務(wù)處理的強(qiáng)大渲染任務(wù)控制機(jī)制。愛(ài)掏網(wǎng) - it200.com

1

言歸正傳,盡管Fiber異步渲染的機(jī)制幾乎重寫(xiě)了整個(gè)reconcile的過(guò)程,但通過(guò)源碼分析可以看到對(duì)節(jié)點(diǎn)reconcile的思路與16之前版本基本一致:

在react的16.3.1版本中,會(huì)在頁(yè)面初始化render運(yùn)行過(guò)程中,對(duì)應(yīng)頁(yè)面結(jié)構(gòu)創(chuàng)建FiberNode,通過(guò)child屬性與siblings屬性分別存放子節(jié)點(diǎn)與兄弟節(jié)點(diǎn),同時(shí)使用return屬性標(biāo)記父節(jié)點(diǎn),便于遍歷與修改。愛(ài)掏網(wǎng) - it200.comFiber在update的時(shí)候,會(huì)從原來(lái)的Fiber(我們稱(chēng)為current)clone出一個(gè)新的Fiber(稱(chēng)為alternate)。愛(ài)掏網(wǎng) - it200.com兩個(gè)Fiber diff出的變化(side effect)記錄在alternate上。愛(ài)掏網(wǎng) - it200.com所以一個(gè)組件在更新時(shí)最多會(huì)有兩個(gè)Fiber與其對(duì)應(yīng),在更新結(jié)束后alternate會(huì)取代之前的current的成為新的current節(jié)點(diǎn)。愛(ài)掏網(wǎng) - it200.com



這里略過(guò)Fiber復(fù)雜的構(gòu)建過(guò)程,我們直接來(lái)看在某個(gè)組件需要更新時(shí)的內(nèi)部機(jī)制,也就是組件中setState方法被調(diào)用后,首先會(huì)在該組件對(duì)應(yīng)的Fiber節(jié)點(diǎn)中設(shè)置updateQueue屬性以隊(duì)列的形式存儲(chǔ)更新內(nèi)容,然后從頂端開(kāi)始對(duì)整個(gè)Fiber樹(shù)開(kāi)始進(jìn)行深度遍歷,查找到需要進(jìn)行更新的Fiber節(jié)點(diǎn),判斷的依據(jù)就是該節(jié)點(diǎn)是否有updateQueue中的更新內(nèi)容,如果存在更新,就運(yùn)行我們熟知的shouldUpdateComponent函數(shù)來(lái)判斷,shouldUpdateComponent返回為真,就執(zhí)行componentWillUpdate函數(shù),并根據(jù)其節(jié)點(diǎn)類(lèi)型決定按哪種方式進(jìn)行更新,也就是運(yùn)行reconcile機(jī)制進(jìn)行diff,如果diff的是component節(jié)點(diǎn),待diff完成之后再運(yùn)行l(wèi)ifeCycle中的componentDidUpdate函數(shù)。愛(ài)掏網(wǎng) - it200.com

const shouldUpdate = checkShouldComponentUpdate(
      workInProgress,
      oldProps,
      newProps,
      oldState,
      newState,
      newContext,
    );

    if (shouldUpdate) {
      // 【譯】這是為了支持react-lifecycles-compat的兼容組件
      // 使用新的API的時(shí)候不能調(diào)用非安全的生命周期鉤子
      if (
        !hasNewLifecycles &&
        (typeof instance.UNSAFE_componentWillUpdate === 'function' ||
          typeof instance.componentWillUpdate === 'function')
      ) {
        //開(kāi)始計(jì)時(shí)componentWillUpdate階段
        startPhaseTimer(workInProgress, 'componentWillUpdate');
        //執(zhí)行組件實(shí)例上的componentWillUpdate鉤子
        if (typeof instance.componentWillUpdate === 'function') {
          instance.componentWillUpdate(newProps, newState, newContext);
        }
        if (typeof instance.UNSAFE_componentWillUpdate === 'function') {
          instance.UNSAFE_componentWillUpdate(newProps, newState, newContext);
        }
        //結(jié)束計(jì)時(shí)componentWillUpdate階段
        stopPhaseTimer();
      }
      // 在當(dāng)前工作中的Fiber打上標(biāo)簽,后續(xù)執(zhí)行componentDidUpdate鉤子
      if (typeof instance.componentDidUpdate === 'function') {
        workInProgress.effectTag |= Update;
      }
      if (typeof instance.getSnapshotBeforeUpdate === 'function') {
        workInProgress.effectTag |= Snapshot;
      }
    } else {
      // 【譯】如果當(dāng)前節(jié)點(diǎn)已經(jīng)在更新中,即使我們終止了更新,仍然應(yīng)該執(zhí)行componentDidUpdate鉤子
      if (typeof instance.componentDidUpdate === 'function') {
        if (
          oldProps !== current.memoizedProps ||
          oldState !== current.memoizedState
        ) {
          workInProgress.effectTag |= Update;
        }
      }
      if (typeof instance.getSnapshotBeforeUpdate === 'function') {
        if (
          oldProps !== current.memoizedProps ||
          oldState !== current.memoizedState
        ) {
          workInProgress.effectTag |= Snapshot;
        }
      }
復(fù)制代碼


這里提到,我們?cè)诮M件中setState之后,React會(huì)將其視為dirty節(jié)點(diǎn),在事件流結(jié)束后,找出dirty的組件節(jié)點(diǎn)并進(jìn)行diff,值得注意的是,雖然重新render構(gòu)建一顆新的Virtual DOM樹(shù)不會(huì)觸碰真正的DOM,這里也并沒(méi)有重新創(chuàng)建新的Fiber樹(shù),取而代之的是在每個(gè)Fiber節(jié)點(diǎn)中都設(shè)置了alternate屬性與current屬性來(lái)分別存放用于更新替代與當(dāng)前的節(jié)點(diǎn)版本,只是在重新遍歷整顆樹(shù)后找到dirty的節(jié)點(diǎn)生成新的Fiber節(jié)點(diǎn)用于更新:



正如react官方文檔中描述的一樣,當(dāng)一個(gè)節(jié)點(diǎn)需要被更新時(shí)(shouldComponentUpdate),下一步則需要對(duì)它及其子節(jié)點(diǎn)進(jìn)行shouldComponentUpdate判斷與Reconcile的過(guò)程來(lái)對(duì)節(jié)點(diǎn)進(jìn)行更新,這里我們可以通過(guò)在組件中寫(xiě)入覆蓋的shouldComponentUpdate函數(shù)來(lái)決定是否進(jìn)行更新的邏輯:



Reconcile過(guò)程的核心源代碼起始于reconcileChildFiber函數(shù),主要實(shí)現(xiàn)方式是:根據(jù)傳入組件的類(lèi)型進(jìn)行不同的reconcile過(guò)程,其中最為復(fù)雜的是傳入子組件數(shù)組調(diào)用reconcileChildrenArray處理的情況。愛(ài)掏網(wǎng) - it200.comreconcileChildrenArray函數(shù)在開(kāi)始進(jìn)行新舊子節(jié)點(diǎn)數(shù)組reconcile時(shí),默認(rèn)先按index順序進(jìn)行對(duì)比,由于Fiber節(jié)點(diǎn)本身沒(méi)有設(shè)置向后指針,因此React目前沒(méi)有采取兩端同時(shí)對(duì)比的算法,也就是說(shuō)每一個(gè)同層級(jí)別的兄弟Fiber節(jié)點(diǎn)只能指向下一個(gè)節(jié)點(diǎn)。愛(ài)掏網(wǎng) - it200.com因此在通常情況下,對(duì)比過(guò)程中react只會(huì)調(diào)用updateSlot將得到的新Fiber數(shù)據(jù)按其不同類(lèi)型直接更新到舊Fiber的位置中。愛(ài)掏網(wǎng) - it200.com

在按順序?qū)Ρ戎校绻褂胾pdateSlot未發(fā)現(xiàn)key值不相等的情況,則進(jìn)行將老節(jié)點(diǎn)替換成為新節(jié)點(diǎn),第一輪遍歷完成后,則判斷如果是新節(jié)點(diǎn)已遍歷完成,就將剩余的老節(jié)點(diǎn)批量刪除,如果是老節(jié)點(diǎn)遍歷完成仍有新節(jié)點(diǎn)剩余,則將新節(jié)點(diǎn)批量插入老節(jié)點(diǎn)末端,如果在第一輪遍歷中發(fā)現(xiàn)key值不相等的情況,則直接跳出以上步驟,按照key值進(jìn)行遍歷更新,最后再刪除沒(méi)有被上述情況涉及的元素,由此可見(jiàn)在列表結(jié)構(gòu)的組件中,添加key值是有助于提升diff算法效率的。愛(ài)掏網(wǎng) - it200.com

以下是reconcileChildrenArray函數(shù)源代碼:

// react使用flow進(jìn)行類(lèi)型檢查
function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;

    for (; oldFiber !== null && newIdx if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
      // 指向下一個(gè)舊的兄弟節(jié)點(diǎn)
        nextOldFiber = oldFiber.sibling;
      }
      // 嘗試使用新的Fiber更新舊節(jié)點(diǎn)
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        expirationTime,
      );
、    //如果在遍歷中發(fā)現(xiàn)key值不相等的情況,則直接跳出第一輪遍歷
      if (newFiber === null) {
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
         // 【譯】我們找到了匹配的節(jié)點(diǎn),但我們并不保留當(dāng)前的Fiber,所以我們需要?jiǎng)h除當(dāng)前的子節(jié)點(diǎn)
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // 記錄上一個(gè)更新的子節(jié)點(diǎn)
      if (previousNewFiber === null) {  
        resultingFirstChild = newFiber;
      } else { 
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // 【譯】我們已經(jīng)遍歷完了所有的新節(jié)點(diǎn),直接刪除剩余舊節(jié)點(diǎn)
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // 如果舊節(jié)點(diǎn)先遍歷完,則按順序插入剩余的新節(jié)點(diǎn),這里受限于Fiber的結(jié)構(gòu)比較繁瑣
      for (; newIdx returnFiber,
          newChildren[newIdx],
          expirationTime,
        );
        if (!newFiber) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // 【譯】把子節(jié)點(diǎn)都設(shè)置快速查找的map映射集
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // 【譯】使用map查找需要保存或刪除的節(jié)點(diǎn)
    for (; newIdx returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // 【譯】新的Fiber也是一個(gè)工作線程,但是如果已有當(dāng)前的實(shí)例,那我們就可以復(fù)用這個(gè)Fiber,
            // 我們要從列表中刪除這個(gè)新的,避免準(zhǔn)備復(fù)用的Fiber被刪除
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        // 插入當(dāng)前更新位置
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) 
      // 【譯】到此所有剩余的子節(jié)點(diǎn)都將被刪除,加入刪除隊(duì)列
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }
    //最終返回Fiber子節(jié)點(diǎn)列表的第一個(gè)節(jié)點(diǎn)
    return resultingFirstChild;
  }
復(fù)制代碼


VirtualDOM的設(shè)計(jì)是提升前端渲染性能的有效方案,也因此提供了以數(shù)據(jù)為驅(qū)動(dòng)的前端框架工具的基礎(chǔ),將我們從DOM的繁瑣操作中解放出來(lái),不同的VirtualDOM方案在diff方面基本基于三條diff原則,具體diff過(guò)程則考慮自身運(yùn)行上下文中的數(shù)據(jù)結(jié)構(gòu),算法效率,組件生命周期與設(shè)計(jì)來(lái)選擇diff實(shí)現(xiàn)。愛(ài)掏網(wǎng) - it200.com例如上文snabbdom的updateChildren執(zhí)行中使用了兩端同時(shí)對(duì)比以及根據(jù)位置順序進(jìn)行移動(dòng)的更新策略,而React則受限于Fiber的單向結(jié)構(gòu)采用按順序直接替換的方式更新,但React優(yōu)化的組件設(shè)計(jì)與Fiber的工作線程機(jī)制在整體渲染性能方面帶來(lái)了效率提升,同時(shí)兩者都提供了基于key值進(jìn)行diff的策略改善方式。愛(ài)掏網(wǎng) - it200.com

VirtualDOM的設(shè)計(jì)影響深遠(yuǎn),本文僅對(duì)VirtualDOM中的思想與diff實(shí)現(xiàn)進(jìn)行了詳細(xì)介紹,此外,如何創(chuàng)建一個(gè)VirtualDOM樹(shù),如何將diff結(jié)果進(jìn)行patch更新等內(nèi)容仍有許多不同的具體實(shí)現(xiàn)方式可以進(jìn)行探索,以及React16的Fiber機(jī)制更是在異步渲染方面上又進(jìn)了一步,值得我們持續(xù)關(guān)注與學(xué)習(xí)。愛(ài)掏網(wǎng) - it200.com


diff算法類(lèi):

snabbdom源碼

React-less Virtual DOM with Snabbdom :functions everywhere!

解析 snabbdom 源碼,教你實(shí)現(xiàn)精簡(jiǎn)的 Virtual DOM 庫(kù)

React’s diff algorithm

Snabbdom - a Virtual DOM Focusing on Simplicity - Interview with Simon Friis Vindum

去哪兒網(wǎng)迷你React的研發(fā)心得


Fiber介紹類(lèi)

React Fiber Architecture

如何理解 React Fiber 架構(gòu)?

React 16 Fiber源碼速覽

How React Fiber can make your web and mobile apps smoother and more responsive

React的新引擎—React Fiber是什么?


原文發(fā)布時(shí)間為:2024年6月10日 原文作者:百度外賣(mài)大前端技術(shù)團(tuán)隊(duì) 本文來(lái)源:掘金如需轉(zhuǎn)載請(qǐng)聯(lián)系原作者
聲明:所有內(nèi)容來(lái)自互聯(lián)網(wǎng)搜索結(jié)果,不保證100%準(zhǔn)確性,僅供參考。如若本站內(nèi)容侵犯了原著者的合法權(quán)益,可聯(lián)系我們進(jìn)行處理。
發(fā)表評(píng)論
更多 網(wǎng)友評(píng)論0 條評(píng)論)
暫無(wú)評(píng)論

返回頂部

主站蜘蛛池模板: 久久久www成人免费精品| 蜜中蜜3在线观看视频| 国产男女野战视频在线看| 欧美老熟妇乱大交xxxxx| 奶交性视频欧美| 日韩欧美亚洲另类| 中文字幕一区二区三区日韩精品 | 国产成人www免费人成看片| 国产在线精品无码二区二区| 亚洲中文字幕无码av永久| 91精品国产福利在线观看| 真实国产乱子伦精品免费| 有色视频在线观看免费高清| 国产精品老熟女露脸视频| 亚洲成a人v欧美综合天堂麻豆| 五月婷婷婷婷婷| AAAAA级少妇高潮大片免费看| 男女猛烈xx00免费视频试看| 天天干天天干天天操| 国产一区韩国女主播| 久久99国产精品久久99| 舔舔小核欲成欢| 性无码一区二区三区在线观看| 免费的成人a视频在线观看| 久久精品免费电影| 调教家政妇第38话无删减| 无码精品久久久久久人妻中字 | 国产乱子伦一区二区三区| 中文字幕在线不卡| 类似爱情1未删减版视频| 在线国产一区二区| 全彩里番acg里番本子| 久久99精品久久久久久噜噜| 经典三级在线播放线观看| 女性高爱潮视频| 亚洲日韩中文字幕在线播放| 国产成人三级视频在线观看播放| 欧美精品www| 国产无遮挡又黄又爽免费网站 | 丰满熟妇乱又伦| 秋霞鲁丝片无码av|