虛擬DOM+diff算法解讀

為何采用虛擬DOM

尤雨溪曾在知乎正面的回答這個(gè)問(wèn)題:

  1. 為函數(shù)式的 UI 編程方式打開(kāi)了大門(mén);
  2. 可以渲染到 DOM 以外的 backend,比如 ReactNative封寞。

針對(duì)這兩點(diǎn)談?wù)勛约旱睦斫猓?/p>

  1. 為函數(shù)式的 UI 編程方式打開(kāi)了大門(mén);

    • 虛擬DOM是由react引入的概念颖榜,react的核心理念就是函數(shù)式編程,類(lèi)似ui = f (a)煤裙,其中a是數(shù)據(jù)掩完,函數(shù)f則是react的render,每個(gè)a的改動(dòng)硼砰,都會(huì)重新生成新的ui且蓬。
    • 每次生成新的ui就需要重新刷新頁(yè)面代價(jià)太過(guò)昂貴,虛擬DOM以及diff算法的引入可以最大限度的復(fù)用舊的DOM题翰,使得渲染性能大幅提升恶阴。
  2. 可以渲染到 DOM 以外的 backend诈胜,比如 ReactNative。

    • 有了虛擬DOM就可以輕松實(shí)現(xiàn)跨平臺(tái)冯事,多平臺(tái)的core都相同焦匈,只是在render到具體平臺(tái)的時(shí)候采取不同的render就好了

真實(shí)DOM的操作

diff算法最后的結(jié)果還是要落到對(duì)真實(shí)DOM的操作上去,這種操作有三種:新增昵仅、刪除缓熟、移位。這三種操作都需要獲取自身DOMparentDOM摔笤,以vue的源碼來(lái)解釋?zhuān)?/p>

  • 新增够滑、移位操作可以通過(guò)如下實(shí)現(xiàn)
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
  • 移位操作則通過(guò)insert實(shí)現(xiàn)
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

diff算法

diff算法我認(rèn)為包括三部分:虛擬DOM樹(shù)的遍歷、parent節(jié)點(diǎn)下的children的比較吕世、diff完成之后對(duì)真實(shí)DOM的操作時(shí)機(jī)

虛擬DOM的遍歷:

虛擬DOM說(shuō)到底只是一顆樹(shù)形結(jié)構(gòu)彰触,對(duì)于樹(shù)的遍歷我們知道有深度遍歷和廣度遍歷

深度遍歷需要棧結(jié)構(gòu),可以通過(guò)遞歸(內(nèi)核維護(hù)調(diào)用棧)的方式實(shí)現(xiàn)命辖,也可以采用人為構(gòu)造棧况毅,然后循環(huán)棧完成深度遍歷。通常深度優(yōu)先搜索法不全部保留結(jié)點(diǎn)吮龄,擴(kuò)展完的結(jié)點(diǎn)從棧中彈出刪去俭茧,這樣咆疗,在棧中存儲(chǔ)的結(jié)點(diǎn)數(shù)就是深度值漓帚,因此它占用空間較少

廣度遍歷則采用隊(duì)列的方式實(shí)現(xiàn)午磁,由于廣度優(yōu)先是按照樹(shù)的層級(jí)來(lái)遍歷的尝抖,在遍歷某層的時(shí)候需要將下一層的數(shù)據(jù)推進(jìn)隊(duì)列里面,所以隊(duì)列的長(zhǎng)度通常會(huì)比樹(shù)的寬度還要寬迅皇。

目前昧辽,不管是vue還是react,采用的都是深度遍歷算法

vue2.0

vue的渲染主要分三部分:

  • 虛擬樹(shù)的遍歷
  • 子節(jié)點(diǎn)的diff
  • 真實(shí)DOM的更新

虛擬樹(shù)的遍歷:
采用遞歸的先序深度遍歷算法

子節(jié)點(diǎn)的diff:
對(duì)于相同的節(jié)點(diǎn)登颓,繼續(xù)比較子節(jié)點(diǎn):

  • 同一級(jí)子元素新老虛擬DOM列表分別設(shè)置startIndexendIndex搅荞,并交叉判斷startIndexendIndex是否是相同元素
  • 對(duì)比的結(jié)果有三種情況:新增、刪除框咙、移位
  • 新增:老的startIndex不動(dòng)咕痛,新的startIndex移位,并在老的startIndex元素前插入
  • 移位:
    • startIndex和老startIndex或者新endIndex和老endIndex相同喇嘱,只要移動(dòng)startIndex或者endIndex就可以了茉贡;
    • startIndex和老endIndex相同,新startIndex++者铜,老endIndex--腔丧,將老endIndexele插入到老startIndexele前面
    • endIndex和老startIndex相同放椰,新endIndex--,老startIndex++愉粤,將老的startIndexele插入到老endIndexele后面
    • startIndexkey匹配到老的vnodekey砾医,將老vnodeele插入到老startIndexele前面,還有一個(gè)操作:將老vnode標(biāo)記位undefined衣厘,(oldCh[idxInOld] = undefined)藻烤,
  • 刪除
    • 等新startIndex和新endIndex合攏,老startIndex和老endIndex之間的非undefinedvnodeele全部刪除头滔,undefinednode代表已經(jīng)處理過(guò)了(移位)

真實(shí)DOM的更新

  • 由于采用遞歸的方式處理vnode怖亭,所以節(jié)點(diǎn)更新真實(shí)DOM的時(shí)機(jī)是該節(jié)點(diǎn)下所有子節(jié)點(diǎn)更新完畢后才會(huì)更新,即從下而上
  • 節(jié)點(diǎn)的props的處理是早于子節(jié)點(diǎn)的diff坤检,所以props的更新是從上而下

vue2.0思維導(dǎo)圖

react16

react的渲染雖然采用深度遍歷兴猩,但是是非遞歸方式,而是采用鏈表的方式早歇,這樣做的原因是方便fiber的引入

react的渲染可以分為兩個(gè)部分:

  • reconciliation 階段
  • commit 階段

reconciliation 階段:

react樹(shù)的reconciliation

所謂的reconciliation階段就是虛擬DOM的diff階段倾芝,由于采用了遞歸的鏈表結(jié)構(gòu),所以每個(gè)節(jié)點(diǎn)必然經(jīng)歷兩次的遍歷箭跳,這兩次的遍歷分別為:beginWorkcompleteUnitOfWork晨另。

  • beginWork:完成對(duì)子節(jié)點(diǎn)的diff過(guò)程(新增,刪除谱姓,移位)借尿,并給相應(yīng)的vnode打上effectTag,返回第一個(gè)子節(jié)點(diǎn)屉来。

通過(guò)唯一 key 可以判斷新老集合中是否存在相同的節(jié)點(diǎn)路翻,if (prevChild === nextChild),如果存在相同節(jié)點(diǎn)茄靠,則進(jìn)行移動(dòng)操作茂契,但在移動(dòng)前需要將當(dāng)前節(jié)點(diǎn)在老集合中的位置與 lastIndex 進(jìn)行比較,if (child._mountIndex < lastIndex)慨绳,則進(jìn)行節(jié)點(diǎn)移動(dòng)操作掉冶,否則不執(zhí)行該操作。這是一種順序優(yōu)化手段脐雪,lastIndex 一直在更新厌小,表示訪問(wèn)過(guò)的節(jié)點(diǎn)在老集合中最右的位置(即最大的位置),如果新集合中當(dāng)前訪問(wèn)的節(jié)點(diǎn)比 lastIndex 大喂江,說(shuō)明當(dāng)前訪問(wèn)節(jié)點(diǎn)在老集合中就比上一個(gè)節(jié)點(diǎn)位置靠后召锈,則該節(jié)點(diǎn)不會(huì)影響其他節(jié)點(diǎn)的位置,因此不用添加到差異隊(duì)列中获询,即不執(zhí)行移動(dòng)操作涨岁,只有當(dāng)訪問(wèn)的節(jié)點(diǎn)比 lastIndex 小時(shí)拐袜,才需要進(jìn)行移動(dòng)操作。

  • completeUnitOfWork:完成對(duì)當(dāng)前節(jié)點(diǎn)的副作用的收集(主要的props的改動(dòng))梢薪,并將所有需要改動(dòng)的節(jié)點(diǎn)串成一個(gè)鏈表effect-list蹬铺,掛到hostRoot節(jié)點(diǎn)上,返回兄弟節(jié)點(diǎn)或者是父節(jié)點(diǎn)秉撇。

reconciliation階段的最終結(jié)果是產(chǎn)生一個(gè)effect-list列表甜攀,這個(gè)effect-list列表里面的節(jié)點(diǎn)的兩個(gè)屬性標(biāo)明接下來(lái)的commit階段需要對(duì)該節(jié)點(diǎn)進(jìn)行的處理:effectTag以及updateQueueeffectTag表明對(duì)節(jié)點(diǎn)位置的改動(dòng)琐馆,updateQueue表明對(duì)節(jié)點(diǎn)狀態(tài)的改動(dòng)

commit 階段
commit階段主要是循環(huán)effect-list來(lái)對(duì)節(jié)點(diǎn)分別處理规阀,并且對(duì)effect-list進(jìn)行了三次循環(huán)

  • 第一遍:執(zhí)行所有的(effectTag & Snapshot)為true的節(jié)點(diǎn)的commitBeforeMutationLifeCycles,即執(zhí)行周期函數(shù)getSnapshotBeforeUpdate

  • 第二遍:為所有(effectTag & (Placement | Update | Deletion))即移位瘦麸、更新谁撼、刪除的節(jié)點(diǎn)進(jìn)行commitPlacement以及commitWorkcommitPlacement做的就是移位和刪除的動(dòng)作滋饲,commitWork則將節(jié)點(diǎn)的updateQueue拿出來(lái)更新

  • 第三遍:為所有的(effectTag & (Update | Callback))的節(jié)點(diǎn)進(jìn)行commitLifeCycles厉碟,即執(zhí)行周期函數(shù)componentDidUpdate

react16思維導(dǎo)圖

fiber節(jié)點(diǎn)參考:

const fiber = {
  // Instance
  tag = tag; // fiber 的類(lèi)型。 
    - IndeterminateComponent
    - FunctionalComponent
    - ClassComponent // Menu, Table
    - HostRoot // ReactDOM.render 的第二個(gè)參數(shù)
    - HostPortal
    - HostComponent // div, span
    - HostText // 純文本節(jié)點(diǎn)屠缭,即 dom  的 nodeName 等于 '#text'
    - CallComponent // 對(duì)應(yīng) call return 中的 call
    - CallHandlerPhase // call 中的 handler 階段
    - ReturnComponent // 對(duì)應(yīng) call return 中的 return
    - Fragment 
    - Mode // AsyncMode || StrictMode
    - ContextConsumer
    - ContextProvider
    - ForwardRef
  key = key; // fiber 的唯一標(biāo)識(shí)
  type = null; // 與 react element 里的 type 一致
  stateNode = null; // 對(duì)應(yīng)組件或者 dom 的實(shí)例

  // Fiber
  return = null; // 等價(jià)于棧幀中的函數(shù)調(diào)用后的返回地址箍鼓,這里即是父 fiber
  child = null; // 即組件的 render 的返回值,是一個(gè)單鏈表(因?yàn)榉祷刂挡灰欢ㄊ且粋€(gè)單一的元素)
  sibling = null; // 單鏈表
  index = 0;

  ref = null;

  // props 等價(jià)于一個(gè)函數(shù)的 arguments
  pendingProps = pendingProps; // 新的 props(要么是當(dāng)前的 props呵曹,要么是 wip 的 props)款咖,默認(rèn)就等于 element.props,對(duì)于 Fragment 和 Portal逢并,則等于 props.children
  memoizedProps = null; // 舊的 props之剧,等于 wipFiber.pendingProps 或者 wipFiber.pendingProps.children
    - 一般 oldProps = workInProgress.memoizedProps
    - 一般 newProps = workInProgress.pendingProps
  updateQueue = null; // 狀態(tài)更新和回調(diào)的函數(shù)隊(duì)列
  memoizedState = null; // 組件實(shí)例的 state

  mode = mode; // 用于描述處理 fiber 和它的子樹(shù)的方式郭卫,創(chuàng)建后就不應(yīng)被改變砍聊,如未指定則從父 fiber 繼承。NoContext || AsyncMode || StrictMode

  // Effects
  effectTag = NoEffect; // 當(dāng)需要變化的時(shí)候贰军,具體需要進(jìn)行的操作的類(lèi)型
    - NoEffect // 初始值
    - PerformedWork // 開(kāi)始處理后置為 PerformedWork
    - Placement // 插入玻蝌,保持原位,移動(dòng) dom 節(jié)點(diǎn)
    - Update // 對(duì) dom 結(jié)構(gòu)的改變词疼。mount 或者 update 后置為 Update
    - PlacementAndUpdate
    - Deletion
    - ContentReset // 將一個(gè)只包含字符串的 dom 節(jié)點(diǎn)俯树,或者 textarea,或者 dangerouslySetInnerHTML 替換成其它類(lèi)型的節(jié)點(diǎn)
    - Callback // setState 的回調(diào)
    - DidCapture // 渲染出錯(cuò)贰盗,準(zhǔn)備捕獲出錯(cuò)信息
    - Ref // 準(zhǔn)備執(zhí)行 ref 回調(diào)
    - ErrLog // 渲染出錯(cuò)许饿,執(zhí)行 componentDidCatch
    - Snapshot // getSnapshotBeforeUpdate
    - HostEffectMask // 暫時(shí)沒(méi)用
    - Incomplete // 任何造成 fiber 的工作無(wú)法完成的情況
    - ShouldCapture //  需要處理錯(cuò)誤
    
  nextEffect = null; // 下一個(gè)需要處理的有副作用的 fiber

  // 本 fiber 的子樹(shù)中有副作用的第一個(gè)和最后一個(gè) fiber
  firstEffect = null; // 
  lastEffect = null; //

  expirationTime = NoWork; // 將來(lái)的某個(gè)時(shí)間點(diǎn),在那之前必須完成所有工作

  alternate = null; // WIP 樹(shù)里面的 fiber舵盈,如果不在更新期間陋率,那么就等于當(dāng)前的 fiber球化,如果是新創(chuàng)建的節(jié)點(diǎn),那么就沒(méi)有 alternate
  
  // dev mode
  _debugID; // 自增的標(biāo)識(shí)每一個(gè) fiber 的 id
  _debugSource = null; // 文件名和行數(shù)瓦糟,與 React Element 的 source 一致
  _debugOwner = null; // 與 React Element 的 owner 一致
  _debugIsCurrentlyTiming = false;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筒愚,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子菩浙,更是在濱河造成了極大的恐慌巢掺,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劲蜻,死亡現(xiàn)場(chǎng)離奇詭異陆淀,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)先嬉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)倔约,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人坝初,你說(shuō)我怎么就攤上這事浸剩。” “怎么了鳄袍?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵绢要,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我拗小,道長(zhǎng)重罪,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任哀九,我火速辦了婚禮剿配,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阅束。我一直安慰自己呼胚,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開(kāi)白布息裸。 她就那樣靜靜地躺著蝇更,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呼盆。 梳的紋絲不亂的頭發(fā)上年扩,一...
    開(kāi)封第一講書(shū)人閱讀 51,215評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音访圃,去河邊找鬼厨幻。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的况脆。 我是一名探鬼主播平绩,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼漠另!你這毒婦竟也來(lái)了捏雌?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笆搓,失蹤者是張志新(化名)和其女友劉穎性湿,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體满败,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡肤频,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了算墨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵荒。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖净嘀,靈堂內(nèi)的尸體忽然破棺而出报咳,到底是詐尸還是另有隱情,我是刑警寧澤挖藏,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布暑刃,位于F島的核電站,受9級(jí)特大地震影響膜眠,放射性物質(zhì)發(fā)生泄漏岩臣。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一宵膨、第九天 我趴在偏房一處隱蔽的房頂上張望架谎。 院中可真熱鬧,春花似錦辟躏、人聲如沸谷扣。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)抑钟。三九已至,卻和暖如春野哭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背幻件。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工拨黔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绰沥。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓篱蝇,卻偏偏與公主長(zhǎng)得像贺待,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子零截,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容