網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
前言
這篇文章幫助大家梳理一下React中的dom-diff。在React中,根據(jù)新的虛擬DOM的不同,分為單節(jié)點(diǎn)(指的是同層級(jí)只有一個(gè)子節(jié)點(diǎn)),和多節(jié)點(diǎn)(指的是同層級(jí)有多個(gè)子節(jié)點(diǎn)),分別是在reconcileSingleElement
和reconcileChildrenArray
中進(jìn)行的。下面結(jié)合源碼和原理圖進(jìn)行詳解。
單節(jié)點(diǎn)
單節(jié)點(diǎn)的dom-diff是在reconcileSingleElement
中進(jìn)行的,而能否復(fù)用的判斷依據(jù)就是將要更新的虛擬DOM的key
和HTML元素的類型(即div
和 p
的區(qū)別)是否和當(dāng)前(頁(yè)面上正在渲染的)真實(shí)DOM的fiber一致。
如圖所示,對(duì)于單節(jié)點(diǎn)的diff我們按照?qǐng)D中的流程,結(jié)合源碼進(jìn)行一一解讀
/** * * @param {*} returnFiber 根fiber div#root對(duì)應(yīng)的fiber * @param {*} currentFirstChild 老的FunctionComponent對(duì)應(yīng)的fiber * @param {*} element 新的虛擬DOM對(duì)象 * @returns 返回新的第一個(gè)子fiber */ function reconcileSingleElement(returnFiber, currentFirstChild, element) { //新的虛擬DOM的key,也就是唯一標(biāo)準(zhǔn) const key = element.key; // null let child = currentFirstChild; //老的FunctionComponent對(duì)應(yīng)的fiber while (child !== null) { //判斷此老fiber對(duì)應(yīng)的key和新的虛擬DOM對(duì)象的key是否一樣 null===null if (child.key === key) { //判斷老fiber對(duì)應(yīng)的類型和新虛擬DOM元素對(duì)應(yīng)的類型是否相同 if (child.type === element.type) {// p div deleteRemainingChildren(returnFiber, child.sibling); //如果key一樣,類型也一樣,則認(rèn)為此節(jié)點(diǎn)可以復(fù)用 const existing = useFiber(child, element.props); existing.ref = element.ref; existing.return = returnFiber; return existing; } else { //如果找到一key一樣老fiber,但是類型不一樣,不能此老fiber,把剩下的全部刪除 deleteRemainingChildren(returnFiber, child); } } else { deleteChild(returnFiber, child); } child = child.sibling; } //因?yàn)槲覀儸F(xiàn)實(shí)的初次掛載,老節(jié)點(diǎn)currentFirstChild肯定是沒(méi)有的,所以可以直接根據(jù)虛擬DOM創(chuàng)建新的Fiber節(jié)點(diǎn) const created = createFiberFromElement(element); created.ref = element.ref; created.return = returnFiber; return created; }
key相同,類型相同
<div> <div key='A'>A</div> <div key='B'>B</div> </div> <!-- 變化到 --> <div> <div key='A'>C</div> </div>
對(duì)于上面列舉到的情況,新的虛擬DOM匹配到第一個(gè)即為相同key和type,我們首先通過(guò)deleteRemainingChildren
方法刪除掉其它的多余的子節(jié)點(diǎn)(上面的 <div key='B'>B</div>
),然后通過(guò)useFiber
方法來(lái)復(fù)用老fiber產(chǎn)生新的fiber,這樣就完成我們的復(fù)用。
key不同,類型相同
<div> <div key='A'>A</div> <div key='B'>B</div> </div> <!-- 變化到 --> <div> <div key='C'>C</div> </div>
對(duì)于上面列舉到的情況,新的虛擬DOM匹配到第一個(gè)即為不同key即使type相同也不會(huì)往下進(jìn)行,通過(guò)deleteChild
方法刪掉第一個(gè)子節(jié)點(diǎn),即<div key='A'>A</div>
對(duì)應(yīng)的fiber,然后再對(duì)第二個(gè)子節(jié)點(diǎn)<div key='B'>B</div>
進(jìn)行對(duì)比,發(fā)現(xiàn)key依然不同,繼續(xù)刪除,刪除完成之后child === null
成立,跳出while
循環(huán),通過(guò)createFiberFromElement
方法根據(jù)新的虛擬DOM創(chuàng)建新的fiber。
key相同,類型不同
<div> <div key='A'>A</div> <div key='B'>B</div> </div> <!-- 變化到 --> <div> <p key='A'>C</p> </div>
對(duì)于上面列舉的情況,第一次匹配到了相同的key但是type不同,依舊是不符合復(fù)用的條件,而且此時(shí)會(huì)通過(guò)deleteRemainingChildren
方法刪除掉所有子節(jié)點(diǎn),即不會(huì)再進(jìn)行第二次比較,直接就跳出循環(huán),通過(guò)createFiberFromElement
方法根據(jù)新的虛擬DOM創(chuàng)建新的fiber。
多節(jié)點(diǎn)
多節(jié)點(diǎn)的diff相對(duì)于單節(jié)點(diǎn)的diff來(lái)說(shuō)更加復(fù)雜一些。這里主要是在方法reconcileChildrenArray
中進(jìn)行,這個(gè)過(guò)程最多會(huì)經(jīng)歷三次遍歷,每次完成相應(yīng)的功能,下面我們結(jié)合源碼來(lái)具體探究一下。
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) { let resultingFirstChild = null; //返回的第一個(gè)新兒子 let previousNewFiber = null; //上一個(gè)的一個(gè)新的兒fiber let newIdx = 0;//用來(lái)遍歷新的虛擬DOM的索引 let oldFiber = currentFirstChild;//第一個(gè)老fiber let nextOldFiber = null;//下一個(gè)第fiber let lastPlacedIndex = 0;//上一個(gè)不需要移動(dòng)的老節(jié)點(diǎn)的索引 // 開(kāi)始第一輪循環(huán) 如果老fiber有值,新的虛擬DOM也有值 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { //先暫下一個(gè)老fiber nextOldFiber = oldFiber.sibling; //試圖更新或者試圖復(fù)用老的fiber const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]); if (newFiber === null) { break; } //如果有老fiber,但是新的fiber并沒(méi)有成功復(fù)用老fiber和老的真實(shí)DOM,那就刪除老fiber,在提交階段會(huì)刪除真實(shí)DOM if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); } //指定新fiber的位置 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C) } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber } //新的虛擬DOM已經(jīng)循環(huán)完畢 if (newIdx === newChildren.length) { //刪除剩下的老fiber deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null) { //如果老的 fiber已經(jīng)沒(méi)有了, 新的虛擬DOM還有,進(jìn)入插入新節(jié)點(diǎn)的邏輯 for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild(returnFiber, newChildren[newIdx]); if (newFiber === null) continue; lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //如果previousNewFiber為null,說(shuō)明這是第一個(gè)fiber if (previousNewFiber === null) { resultingFirstChild = newFiber; //這個(gè)newFiber就是大兒子 } else { //否則說(shuō)明不是大兒子,就把這個(gè)newFiber添加上一個(gè)子節(jié)點(diǎn)后面 previousNewFiber.sibling = newFiber; } //讓newFiber成為最后一個(gè)或者說(shuō)上一個(gè)子fiber previousNewFiber = newFiber; } } // 開(kāi)始處理移動(dòng)的情況 const existingChildren = mapRemainingChildren(returnFiber, oldFiber); //開(kāi)始遍歷剩下的虛擬DOM子節(jié)點(diǎn) for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]); if (newFiber !== null) { //如果要跟蹤副作用,并且有老fiber if (newFiber.alternate !== null) { existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key); } //指定新的fiber存放位置 ,并且給lastPlacedIndex賦值 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; //這個(gè)newFiber就是大兒子 } else { //否則說(shuō)明不是大兒子,就把這個(gè)newFiber添加上一個(gè)子節(jié)點(diǎn)后面 previousNewFiber.sibling = newFiber; } //讓newFiber成為最后一個(gè)或者說(shuō)上一個(gè)子fiber previousNewFiber = newFiber; } } //等全部處理完后,刪除map中所有剩下的老fiber existingChildren.forEach(child => deleteChild(returnFiber, child)); return resultingFirstChild; }
這段代碼是比較長(zhǎng)的,這里全部貼出來(lái)就是體現(xiàn)其完整性。下面幫助大家逐步的分析。
<ul key="container"> <li key="A">A</li> <li key="B">B</li> <li key="C">C</li> <li key="D">D</li> <li key="E">E</li> <li key="F">F</li> </ul> <!-- 變化到 --> <ul key="container"> <li key="A">A2</li> <li key="C">C2</li> <li key="E">E2</li> <li key="B">B2</li> <li key="G">G</li> <li key="D">D2</li> </ul>
第一次遍歷
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { //先暫下一個(gè)老fiber nextOldFiber = oldFiber.sibling; //試圖更新或者試圖復(fù)用老的fiber const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]); if (newFiber === null) { break; } if (shouldTrackSideEffects) { //如果有老fiber,但是新的fiber并沒(méi)有成功復(fù)用老fiber和老的真實(shí)DOM,那就刪除老fiber,在提交階段會(huì)刪除真實(shí)DOM if (oldFiber && newFiber.alternate === null) { deleteChild(returnFiber, oldFiber); } } //指定新fiber的位置 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber;//li(A).sibling=p(B).sibling=>li(C) } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; oldFiber = nextOldFiber }
我們所有的對(duì)比都是基于新節(jié)點(diǎn)的虛擬DOM和老節(jié)點(diǎn)的fiber,當(dāng)我們對(duì)比A1和A2時(shí),會(huì)根據(jù)updateSlot
方法進(jìn)行條件判斷,發(fā)現(xiàn)他們的key和type相同,符合復(fù)用條件返回創(chuàng)建好的fiber,我們的操作指針都指向下一個(gè)操作節(jié)點(diǎn),開(kāi)始對(duì)下一個(gè)節(jié)點(diǎn)進(jìn)行第一次遍歷。
當(dāng)我們對(duì)比C2和B時(shí),因?yàn)镃2和B的key并不相同,updateSlot
返回null
,第一次遍歷break
開(kāi)始進(jìn)入第二次遍歷。
第二次遍歷
if (oldFiber === null) { //如果老的 fiber已經(jīng)沒(méi)有了, 新的虛擬DOM還有,進(jìn)入插入新節(jié)點(diǎn)的邏輯 for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild(returnFiber, newChildren[newIdx]); if (newFiber === null) continue; lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); //如果previousNewFiber為null,說(shuō)明這是第一個(gè)fiber if (previousNewFiber === null) { resultingFirstChild = newFiber; //這個(gè)newFiber就是大兒子 } else { //否則說(shuō)明不是大兒子,就把這個(gè)newFiber添加上一個(gè)子節(jié)點(diǎn)后面 previousNewFiber.sibling = newFiber; } //讓newFiber成為最后一個(gè)或者說(shuō)上一個(gè)子fiber previousNewFiber = newFiber; } }
然而oldFiber
依舊是存在的,會(huì)直接進(jìn)入到第三次遍歷,但是我們這里帶大家梳理一下,看看是如何操作的。這里的遍歷主要是針對(duì)新節(jié)點(diǎn)還存在,但是老fiber已經(jīng)沒(méi)有了,即新更新的節(jié)點(diǎn)要多余老節(jié)點(diǎn)的情況,我們這里需要做的就是將剩下的新節(jié)點(diǎn)的fiber通過(guò)createChild
創(chuàng)造出來(lái)。
第三次遍歷
// 開(kāi)始處理移動(dòng)的情況 const existingChildren = mapRemainingChildren(returnFiber, oldFiber); //開(kāi)始遍歷剩下的虛擬DOM子節(jié)點(diǎn) for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], ); if (newFiber !== null) { //如果要跟蹤副作用,并且有老fiber if (newFiber.alternate !== null) { existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key); } //指定新的fiber存放位置 ,并且給lastPlacedIndex賦值 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; //這個(gè)newFiber就是大兒子 } else { //否則說(shuō)明不是大兒子,就把這個(gè)newFiber添加上一個(gè)子節(jié)點(diǎn)后面 previousNewFiber.sibling = newFiber; } //讓newFiber成為最后一個(gè)或者說(shuō)上一個(gè)子fiber previousNewFiber = newFiber; } } function mapRemainingChildren(returnFiber, currentFirstChild) { const existingChildren = new Map(); let existingChild = currentFirstChild; while (existingChild != null) { //如果有key用key,如果沒(méi)有key使用索引 if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; }
接下來(lái)我們進(jìn)行第三次遍歷,也就是我們節(jié)點(diǎn)移動(dòng)的情況,這里的復(fù)用是比較復(fù)雜了。
首先我們會(huì)創(chuàng)造一個(gè)Map
來(lái)承接所有的剩余的老節(jié)點(diǎn),接下來(lái)我們會(huì)根據(jù)key,或者index,來(lái)挑選老節(jié)點(diǎn)以供復(fù)用。找到一個(gè)能復(fù)用的節(jié)點(diǎn),就會(huì)在Map
中刪除對(duì)應(yīng)的節(jié)點(diǎn),如果有對(duì)應(yīng)的點(diǎn)就復(fù)用,沒(méi)有就新創(chuàng)建節(jié)點(diǎn)。
- 多個(gè)節(jié)點(diǎn)數(shù)量不同、key 不同;
- 第一輪比較 A 和 A,相同可以復(fù)用,更新,然后比較 B 和 C,key 不同直接跳出第一個(gè)循環(huán);
- 把剩下 oldFiber 的放入 existingChildren 這個(gè) map 中;
- 然后聲明一個(gè)lastPlacedIndex變量,表示不需要移動(dòng)的老節(jié)點(diǎn)的索引;
- 繼續(xù)循環(huán)剩下的虛擬 DOM 節(jié)點(diǎn);
- 如果能在 map 中找到相同 key 相同 type 的節(jié)點(diǎn)則可以復(fù)用老 fiber,并把此老 fiber 從 map 中刪除;
- 如果能在 map 中找不到相同 key 相同 type 的節(jié)點(diǎn)則創(chuàng)建新的 fiber;
- 如果是復(fù)用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果是要移動(dòng)老 fiber,不變;
- 如果是復(fù)用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果否則更新 lastPlacedIndex 為老 fiber 的 index;
- 把所有的 map 中剩下的 fiber 全部標(biāo)記為刪除;
- (刪除#li#F)=>(添加#li#B)=>(添加#li#G)=>(添加#li#D)=>null;
總結(jié)
DOM DIFF 的三個(gè)規(guī)則
- 只對(duì)同級(jí)元素進(jìn)行比較,不同層級(jí)不對(duì)比
- 不同的類型對(duì)應(yīng)不同的元素
- 可以通過(guò) key 來(lái)標(biāo)識(shí)同一個(gè)節(jié)點(diǎn)
第 1 輪遍歷
- 如果 key 不同則直接結(jié)束本輪循環(huán)
- newChildren 或 oldFiber 遍歷完,結(jié)束本輪循環(huán)
- key 相同而 type 不同,標(biāo)記老的 oldFiber 為刪除,繼續(xù)循環(huán)
- key 相同而 type 也相同,則可以復(fù)用老節(jié) oldFiber 節(jié)點(diǎn),繼續(xù)循環(huán)
第 2 輪遍歷
- newChildren 遍歷完而 oldFiber 還有,遍歷剩下所有的 oldFiber 標(biāo)記為刪除,DIFF 結(jié)束
- oldFiber 遍歷完了,而 newChildren 還有,將剩下的 newChildren 標(biāo)記為插入,DIFF 結(jié)束
- newChildren 和 oldFiber 都同時(shí)遍歷完成,diff 結(jié)束
- newChildren 和 oldFiber 都沒(méi)有完成,則進(jìn)行節(jié)點(diǎn)移動(dòng)的邏輯
- 第 3 輪遍歷
處理節(jié)點(diǎn)移動(dòng)的情況
原文鏈接:https://juejin.cn/post/7204285137046782012
- 上一篇:沒(méi)有了
- 下一篇:沒(méi)有了
相關(guān)推薦
- 2022-10-03 Matlab實(shí)現(xiàn)鼠標(biāo)光標(biāo)變成愛(ài)心和瞄準(zhǔn)鏡形狀_C 語(yǔ)言
- 2023-07-31 element中el-input無(wú)法輸入
- 2022-09-29 React?Native?中處理?Android?手機(jī)吞字的解決方案_React
- 2023-03-20 c#判斷代碼是否執(zhí)行超時(shí)的幾種方式總結(jié)_C#教程
- 2022-06-18 Android實(shí)現(xiàn)歷史搜索記錄_Android
- 2022-08-17 C++關(guān)于樹(shù)的定義全面梳理_C 語(yǔ)言
- 2022-11-19 Golang切片Slice功能操作詳情_(kāi)Golang
- 2022-06-01 Apache?Hudi靈活的Payload機(jī)制硬核解析_服務(wù)器其它
- 欄目分類
-
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支