React DOM-diff 節(jié)點源碼解析
前言
這篇文章幫助大家梳理一下React中的dom-diff。在React中,根據(jù)新的虛擬DOM的不同,分為單節(jié)點(指的是同層級只有一個子節(jié)點),和多節(jié)點(指的是同層級有多個子節(jié)點),分別是在reconcileSingleElement和reconcileChildrenArray中進行的。下面結(jié)合源碼和原理圖進行詳解。
單節(jié)點
單節(jié)點的dom-diff是在reconcileSingleElement中進行的,而能否復(fù)用的判斷依據(jù)就是將要更新的虛擬DOM的key和HTML元素的類型(即div 和 p的區(qū)別)是否和當前(頁面上正在渲染的)真實DOM的fiber一致。

如圖所示,對于單節(jié)點的diff我們按照圖中的流程,結(jié)合源碼進行一一解讀
/**
*
* @param {*} returnFiber 根fiber div#root對應(yīng)的fiber
* @param {*} currentFirstChild 老的FunctionComponent對應(yīng)的fiber
* @param {*} element 新的虛擬DOM對象
* @returns 返回新的第一個子fiber
*/
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
//新的虛擬DOM的key,也就是唯一標準
const key = element.key; // null
let child = currentFirstChild; //老的FunctionComponent對應(yīng)的fiber
while (child !== null) {
//判斷此老fiber對應(yīng)的key和新的虛擬DOM對象的key是否一樣 null===null
if (child.key === key) {
//判斷老fiber對應(yīng)的類型和新虛擬DOM元素對應(yīng)的類型是否相同
if (child.type === element.type) {// p div
deleteRemainingChildren(returnFiber, child.sibling);
//如果key一樣,類型也一樣,則認為此節(jié)點可以復(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;
}
//因為我們現(xiàn)實的初次掛載,老節(jié)點currentFirstChild肯定是沒有的,所以可以直接根據(jù)虛擬DOM創(chuàng)建新的Fiber節(jié)點
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>
對于上面列舉到的情況,新的虛擬DOM匹配到第一個即為相同key和type,我們首先通過deleteRemainingChildren方法刪除掉其它的多余的子節(jié)點(上面的 <div key='B'>B</div>),然后通過useFiber方法來復(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>
對于上面列舉到的情況,新的虛擬DOM匹配到第一個即為不同key即使type相同也不會往下進行,通過deleteChild方法刪掉第一個子節(jié)點,即<div key='A'>A</div>對應(yīng)的fiber,然后再對第二個子節(jié)點<div key='B'>B</div>進行對比,發(fā)現(xiàn)key依然不同,繼續(xù)刪除,刪除完成之后child === null成立,跳出while循環(huán),通過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>
對于上面列舉的情況,第一次匹配到了相同的key但是type不同,依舊是不符合復(fù)用的條件,而且此時會通過deleteRemainingChildren方法刪除掉所有子節(jié)點,即不會再進行第二次比較,直接就跳出循環(huán),通過createFiberFromElement方法根據(jù)新的虛擬DOM創(chuàng)建新的fiber。
多節(jié)點
多節(jié)點的diff相對于單節(jié)點的diff來說更加復(fù)雜一些。這里主要是在方法reconcileChildrenArray中進行,這個過程最多會經(jīng)歷三次遍歷,每次完成相應(yīng)的功能,下面我們結(jié)合源碼來具體探究一下。
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
let resultingFirstChild = null; //返回的第一個新兒子
let previousNewFiber = null; //上一個的一個新的兒fiber
let newIdx = 0;//用來遍歷新的虛擬DOM的索引
let oldFiber = currentFirstChild;//第一個老fiber
let nextOldFiber = null;//下一個第fiber
let lastPlacedIndex = 0;//上一個不需要移動的老節(jié)點的索引
// 開始第一輪循環(huán) 如果老fiber有值,新的虛擬DOM也有值
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
//先暫下一個老fiber
nextOldFiber = oldFiber.sibling;
//試圖更新或者試圖復(fù)用老的fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) {
break;
}
//如果有老fiber,但是新的fiber并沒有成功復(fù)用老fiber和老的真實DOM,那就刪除老fiber,在提交階段會刪除真實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)沒有了, 新的虛擬DOM還有,進入插入新節(jié)點的邏輯
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (newFiber === null) continue;
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
//如果previousNewFiber為null,說明這是第一個fiber
if (previousNewFiber === null) {
resultingFirstChild = newFiber; //這個newFiber就是大兒子
} else {
//否則說明不是大兒子,就把這個newFiber添加上一個子節(jié)點后面
previousNewFiber.sibling = newFiber;
}
//讓newFiber成為最后一個或者說上一個子fiber
previousNewFiber = newFiber;
}
}
// 開始處理移動的情況
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//開始遍歷剩下的虛擬DOM子節(jié)點
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; //這個newFiber就是大兒子
} else {
//否則說明不是大兒子,就把這個newFiber添加上一個子節(jié)點后面
previousNewFiber.sibling = newFiber;
}
//讓newFiber成為最后一個或者說上一個子fiber
previousNewFiber = newFiber;
}
}
//等全部處理完后,刪除map中所有剩下的老fiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
return resultingFirstChild;
}
這段代碼是比較長的,這里全部貼出來就是體現(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++) {
//先暫下一個老fiber
nextOldFiber = oldFiber.sibling;
//試圖更新或者試圖復(fù)用老的fiber
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
if (newFiber === null) {
break;
}
if (shouldTrackSideEffects) {
//如果有老fiber,但是新的fiber并沒有成功復(fù)用老fiber和老的真實DOM,那就刪除老fiber,在提交階段會刪除真實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
}
我們所有的對比都是基于新節(jié)點的虛擬DOM和老節(jié)點的fiber,當我們對比A1和A2時,會根據(jù)updateSlot方法進行條件判斷,發(fā)現(xiàn)他們的key和type相同,符合復(fù)用條件返回創(chuàng)建好的fiber,我們的操作指針都指向下一個操作節(jié)點,開始對下一個節(jié)點進行第一次遍歷。
當我們對比C2和B時,因為C2和B的key并不相同,updateSlot返回null,第一次遍歷break開始進入第二次遍歷。
第二次遍歷
if (oldFiber === null) {
//如果老的 fiber已經(jīng)沒有了, 新的虛擬DOM還有,進入插入新節(jié)點的邏輯
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);
if (newFiber === null) continue;
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
//如果previousNewFiber為null,說明這是第一個fiber
if (previousNewFiber === null) {
resultingFirstChild = newFiber; //這個newFiber就是大兒子
} else {
//否則說明不是大兒子,就把這個newFiber添加上一個子節(jié)點后面
previousNewFiber.sibling = newFiber;
}
//讓newFiber成為最后一個或者說上一個子fiber
previousNewFiber = newFiber;
}
}
然而oldFiber依舊是存在的,會直接進入到第三次遍歷,但是我們這里帶大家梳理一下,看看是如何操作的。這里的遍歷主要是針對新節(jié)點還存在,但是老fiber已經(jīng)沒有了,即新更新的節(jié)點要多余老節(jié)點的情況,我們這里需要做的就是將剩下的新節(jié)點的fiber通過createChild創(chuàng)造出來。
第三次遍歷
// 開始處理移動的情況
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
//開始遍歷剩下的虛擬DOM子節(jié)點
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; //這個newFiber就是大兒子
} else {
//否則說明不是大兒子,就把這個newFiber添加上一個子節(jié)點后面
previousNewFiber.sibling = newFiber;
}
//讓newFiber成為最后一個或者說上一個子fiber
previousNewFiber = newFiber;
}
}
function mapRemainingChildren(returnFiber, currentFirstChild) {
const existingChildren = new Map();
let existingChild = currentFirstChild;
while (existingChild != null) {
//如果有key用key,如果沒有key使用索引
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
} else {
existingChildren.set(existingChild.index, existingChild);
}
existingChild = existingChild.sibling;
}
return existingChildren;
}
接下來我們進行第三次遍歷,也就是我們節(jié)點移動的情況,這里的復(fù)用是比較復(fù)雜了。
首先我們會創(chuàng)造一個Map來承接所有的剩余的老節(jié)點,接下來我們會根據(jù)key,或者index,來挑選老節(jié)點以供復(fù)用。找到一個能復(fù)用的節(jié)點,就會在Map中刪除對應(yīng)的節(jié)點,如果有對應(yīng)的點就復(fù)用,沒有就新創(chuàng)建節(jié)點。

- 多個節(jié)點數(shù)量不同、key 不同;
- 第一輪比較 A 和 A,相同可以復(fù)用,更新,然后比較 B 和 C,key 不同直接跳出第一個循環(huán);
- 把剩下 oldFiber 的放入 existingChildren 這個 map 中;
- 然后聲明一個lastPlacedIndex變量,表示不需要移動的老節(jié)點的索引;
- 繼續(xù)循環(huán)剩下的虛擬 DOM 節(jié)點;
- 如果能在 map 中找到相同 key 相同 type 的節(jié)點則可以復(fù)用老 fiber,并把此老 fiber 從 map 中刪除;
- 如果能在 map 中找不到相同 key 相同 type 的節(jié)點則創(chuàng)建新的 fiber;
- 如果是復(fù)用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果是要移動老 fiber,不變;
- 如果是復(fù)用老的 fiber,則判斷老 fiber 的索引是否小于 lastPlacedIndex,如果否則更新 lastPlacedIndex 為老 fiber 的 index;
- 把所有的 map 中剩下的 fiber 全部標記為刪除;
- (刪除#li#F)=>(添加#li#B)=>(添加#li#G)=>(添加#li#D)=>null;
總結(jié)
DOM DIFF 的三個規(guī)則
- 只對同級元素進行比較,不同層級不對比
- 不同的類型對應(yīng)不同的元素
- 可以通過 key 來標識同一個節(jié)點
第 1 輪遍歷
- 如果 key 不同則直接結(jié)束本輪循環(huán)
- newChildren 或 oldFiber 遍歷完,結(jié)束本輪循環(huán)
- key 相同而 type 不同,標記老的 oldFiber 為刪除,繼續(xù)循環(huán)
- key 相同而 type 也相同,則可以復(fù)用老節(jié) oldFiber 節(jié)點,繼續(xù)循環(huán)
第 2 輪遍歷
- newChildren 遍歷完而 oldFiber 還有,遍歷剩下所有的 oldFiber 標記為刪除,DIFF 結(jié)束
- oldFiber 遍歷完了,而 newChildren 還有,將剩下的 newChildren 標記為插入,DIFF 結(jié)束
- newChildren 和 oldFiber 都同時遍歷完成,diff 結(jié)束
- newChildren 和 oldFiber 都沒有完成,則進行節(jié)點移動的邏輯
- 第 3 輪遍歷
處理節(jié)點移動的情況
以上就是React DOM-diff 節(jié)點源碼解析的詳細內(nèi)容,更多關(guān)于React DOM-diff 的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React不能將useMemo設(shè)置為默認方法原因詳解
這篇文章主要為大家介紹了React不能將useMemo設(shè)置為默認方法原因詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪<BR>2022-07-07
react native基于FlatList下拉刷新上拉加載實現(xiàn)代碼示例
這篇文章主要介紹了react native基于FlatList下拉刷新上拉加載實現(xiàn)代碼示例2018-09-09
詳解如何給React-Router添加路由頁面切換時的過渡動畫
這篇文章主要介紹了詳解如何給React-Router添加路由頁面切換時的過渡動畫,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-04-04

