視頻課程的目的是爲了快速掌握react源碼運行的過程和react中的scheduler、reconciler、renderer、fiber等,而且詳細debug源碼和分析,過程更清晰。react
視頻課程:進入課程面試
demos:demo算法
在render階段更新Fiber節點時,咱們會調用reconcileChildFibers對比current Fiber和jsx對象構建workInProgress Fiber,這裏current Fiber是指當前dom對應的fiber樹,jsx是class組件render方法或者函數組件的返回值。數組
在reconcileChildFibers中會根據newChild的類型來進入單節點的diff或者多節點diff緩存
function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, ): Fiber | null {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
//單一節點diff
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
}
}
//...
if (isArray(newChild)) {
//多節點diff
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
// 刪除節點
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
複製代碼
diff過程的主要流程以下圖:markdown
咱們知道對比兩顆樹的複雜度自己是O(n3),對咱們的應用來講這個是不能承受的量級,react爲了下降複雜度,提出了三個前提:併發
只對同級比較,跨層級的dom不會進行復用dom
不一樣類型節點生成的dom樹不一樣,此時會直接銷燬老節點及子孫節點,並新建節點ide
能夠經過key來對元素diff的過程提供複用的線索,例如:函數
const a = (
<> <p key="0">0</p> <p key="1">1</p> </>
);
const b = (
<> <p key="1">1</p> <p key="0">0</p> </>
);
複製代碼
若是a和b裏的元素都沒有key,由於節點的更新先後文本節點不一樣,致使他們都不能複用,因此會銷燬以前的節點,並新建節點,可是如今有key了,b中的節點會在老的a中尋找key相同的節點嘗試複用,最後發現只是交換位置就能夠完成更新,具體對比過程後面會講到。
單點diff有以下幾種狀況:
function reconcileSingleElement( returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement ): Fiber {
const key = element.key;
let child = currentFirstChild;
//child節點不爲null執行對比
while (child !== null) {
// 1.比較key
if (child.key === key) {
// 2.比較type
switch (child.tag) {
//...
default: {
if (child.elementType === element.type) {
// type相同則能夠複用 返回複用的節點
return existing;
}
// type不一樣跳出
break;
}
}
//key相同,type不一樣則把fiber及和兄弟fiber標記刪除
deleteRemainingChildren(returnFiber, child);
break;
} else {
//key不一樣直接標記刪除該節點
deleteChild(returnFiber, child);
}
child = child.sibling;
}
//新建新Fiber
}
複製代碼
多節點diff比較複雜,咱們分三種狀況進行討論,其中a表示更新前的節點,b表示更新後的節點
屬性變化
const a = (
<> <p key="0" name='0'>0</p> <p key="1">1</p> </>
);
const b = (
<> <p key="0" name='00'>0</p> <p key="1">1</p> </>
);
複製代碼
type變化
const a = (
<> <p key="0">0</p> <p key="1">1</p> </>
);
const b = (
<> <div key="0">0</div> <p key="1">1</p> </>
);
複製代碼
新增節點
const a = (
<> <p key="0">0</p> <p key="1">1</p> </>
);
const b = (
<> <p key="0">0</p> <p key="1">1</p> <p key="2">2</p> </>
);
複製代碼
節點刪除
const a = (
<> <p key="0">0</p> <p key="1">1</p> <p key="2">2</p> </>
);
const b = (
<> <p key="0">0</p> <p key="1">1</p> </>
);
複製代碼
節點位置變化
const a = (
<> <p key="0">0</p> <p key="1">1</p> </>
);
const b = (
<> <p key="1">1</p> <p key="0">0</p> </>
);
複製代碼
在源碼中多節點diff會經歷三次遍歷,第一次遍歷處理節點的更新(包括props更新和type更新和刪除),第二次遍歷處理其餘的狀況(節點新增),其緣由在於在大多數的應用中,節點更新的頻率更加頻繁,第三次處理位節點置改變
第一次遍歷
由於老的節點存在於current Fiber中,因此它是個鏈表結構,還記得Fiber雙緩存結構嘛,節點經過child、return、sibling鏈接,而newChildren存在於jsx當中,因此遍歷對比的時候,首先讓newChildren[i]與
oldFiber對比,而後讓i++、nextOldFiber = oldFiber.sibling。在第一輪遍歷中,會處理三種狀況,其中第1,2兩種狀況會結束第一次循環
newChildren遍歷完,oldFiber沒遍歷完,在第一次遍歷完成以後將oldFiber中沒遍歷完的節點標記爲DELETION,即刪除的DELETION Tag
第二次遍歷
第二次遍歷考慮三種狀況
1. newChildren和oldFiber都遍歷完:多節點diff過程結束
2. newChildren沒遍歷完,oldFiber遍歷完,將剩下的newChildren的節點標記爲Placement,即插入的Tag
複製代碼
第三次遍歷
主要邏輯在placeChild函數中,例如更新前節點順序是ABCD,更新後是ACDB
newChild中第一個位置的A和oldFiber第一個位置的A,key相同可複用,lastPlacedIndex=0
newChild中第二個位置的C和oldFiber第二個位置的B,key不一樣跳出第一次循環,將oldFiber中的BCD保存在map中
newChild中第二個位置的C在oldFiber中的index=2 > lastPlacedIndex=0不須要移動,lastPlacedIndex=2
newChild中第三個位置的D在oldFiber中的index=3 > lastPlacedIndex=2不須要移動,lastPlacedIndex=3
newChild中第四個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最後
看圖更直觀
例如更新前節點順序是ABCD,更新後是DABC
newChild中第一個位置的D和oldFiber第一個位置的A,key不相同不可複用,將oldFiber中的ABCD保存在map中,lastPlacedIndex=0
newChild中第一個位置的D在oldFiber中的index=3 > lastPlacedIndex=0不須要移動,lastPlacedIndex=3
newChild中第二個位置的A在oldFiber中的index=0 < lastPlacedIndex=3,移動到最後
newChild中第三個位置的B在oldFiber中的index=1 < lastPlacedIndex=3,移動到最後
newChild中第四個位置的C在oldFiber中的index=2 < lastPlacedIndex=3,移動到最後
看圖更直觀
代碼以下:
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
var current = newFiber.alternate;
if (current !== null) {
var oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
//oldIndex小於lastPlacedIndex的位置 則將節點插入到最後
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
return oldIndex;//不須要移動 lastPlacedIndex = oldIndex;
}
} else {
//新增插入
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
複製代碼
function reconcileChildrenArray( returnFiber: Fiber,//父fiber節點 currentFirstChild: Fiber | null,//childs中第一個節點 newChildren: Array<*>,//新節點數組 也就是jsx數組 lanes: Lanes,//lane相關 第12章介紹 ): Fiber | null {
let resultingFirstChild: Fiber | null = null;//diff以後返回的第一個節點
let previousNewFiber: Fiber | null = null;//新節點中上次對比過的節點
let oldFiber = currentFirstChild;//正在對比的oldFiber
let lastPlacedIndex = 0;//上次可複用的節點位置 或者oldFiber的位置
let newIdx = 0;//新節點中對比到了的位置
let nextOldFiber = null;//正在對比的oldFiber
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {//第一次遍歷
if (oldFiber.index > newIdx) {//nextOldFiber賦值
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(//更新節點,若是key不一樣則newFiber=null
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;//跳出第一次遍歷
}
if (shouldTrackSideEffects) {//檢查shouldTrackSideEffects
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//標記節點插入
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);//將oldFiber中沒遍歷完的節點標記爲DELETION
return resultingFirstChild;
}
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {//第2次遍歷
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//插入新增節點
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 將剩下的oldFiber加入map中
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
for (; newIdx < newChildren.length; newIdx++) {//第三次循環 處理節點移動
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
existingChildren.delete(//刪除找到的節點
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//標記爲插入的邏輯
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
//刪除existingChildren中剩下的節點
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
複製代碼