Анализ исходного кода React, понятный каждому (требуется для высокой зарплаты на крупных фабриках)
8.Алгоритм сравнения (мама больше не беспокоится о моем интервью)
Видео конечно и Debug Demos
Цель видеокурса - быстро освоить процесс запуска исходного кода реакта и планировщика, реконсилера, рендерера, файбера и т.д. в реакте, а также детально отладить исходный код и анализ, и процесс понятнее .
Видео уроки:войти в курс
демо:demo
структура курса:
- Начало (я слышал, что вы все еще боретесь с исходным кодом реакции)
- Ментальная модель React (Давай, давай, пусть мозг будет реагировать на мышление)
- Волокно (я дом в памяти)
- Начните с устаревшего или параллельного (начните с записи, затем давайте перейдем к будущему)
- процесс обновления состояния (что именно происходит в setState)
- этап рендеринга (отлично, у меня есть навыки создания Fiber)
- Этап коммита (я слышал, что рендерер помог нам его разметить, отобразить реальный узел)
- алгоритм diff (мама больше не беспокоится о моем diff-интервью)
- Источник крючков (функциональный компонент хотел знать, как сохранить состояние вещи)
- модель планировщика и полосы (чтобы увидеть, приостановлены ли задачи, возобновлены и поставлены в очередь)
- параллельный режим (что такое параллельный режим)
- Рукописная мини-реакция (короткий и умный - это я)
При обновлении узла Fiber на этапе рендеринга мы будем вызывать reconcileChildFibers для сравнения текущих объектов Fiber и jsx для построения workInProgress Fiber, где current Fiber ссылается на дерево волокон, соответствующее текущему dom, а jsx — это возвращаемое значение метод рендеринга компонента класса или функционального компонента.
В reconcileChildFibers одноузловое или многоузловое различие будет введено в соответствии с типом newChild.
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 выглядит следующим образом:
Мы знаем, что сложность сравнения двух деревьев составляет O(n3), что является невыносимой величиной для нашего приложения.Чтобы уменьшить сложность, react предлагает три предпосылки:
-
Только для однорангового сравнения, межуровневый домен не будет повторно использоваться.
-
Деревья домов, сгенерированные разными типами узлов, различаются.В это время старые узлы и узлы-потомки будут непосредственно уничтожены, а новые узлы будут созданы.
-
Ключи можно использовать для предоставления подсказок мультиплексирования процессу сравнения элементов, например:
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 нет ключа, поскольку текстовые узлы до и после обновления узла различны, их нельзя использовать повторно, поэтому предыдущий узел будет уничтожен и будет создан новый узел, но теперь нет является ключом, узел в b будет искать узлы с тем же ключом в старом a и пытаться использовать их повторно, и, наконец, обнаружил, что обновление может быть завершено, просто заменив позицию, Конкретный процесс сравнения будет обсуждаться позже .
разница в одном узле
Один точка 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 }многоузловая разность
Дифференциал с несколькими узлами более сложен, мы обсудим его в трех случаях, где 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> </> ); -
изменение типа
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> </> );
В исходном коде многоузловое сравнение будет проходить через три обхода.Первый обход обрабатывает обновления узлов (включая обновления свойств, обновления и удаления типов), а второй обход обрабатывает другие случаи (добавления узлов).Причина заключается в том, что в больших приложениях частота обновлений узла более частая, и в третий раз положение узла изменяется
-
первый обход
Поскольку старый узел существует в текущем волокне, он представляет собой структуру связанного списка. Помните о структуре двойного кеша Fiber. Узлы связаны дочерними, возвратными и одноуровневыми узлами, а newChildren существует в jsx, поэтому при обходе и сравнении сначала пусть новые дети [i]
与Сравните oldFiber, затем пусть i++, nextOldFiber = oldFiber.sibling. В первом раунде обхода будут обработаны три кейса, из которых первый и второй кейсы завершат первый цикл.- Ключ другой, первый цикл заканчивается
- После прохождения newChildren или oldFiber первый цикл завершается.
- Ключ отличается от типа, а oldFiber помечен как DELETION
- Один и тот же ключ и тот же тип можно использовать повторно
После завершения обхода newChildren старыйFiber не был пройден.После завершения первого обхода непройденные узлы в oldFiber помечаются как DELETION, то есть удаленный тег DELETION
-
второй обход
Второй обход рассматривает три случая
1. newChildren和oldFiber都遍历完:多节点diff过程结束 2. newChildren没遍历完,oldFiber遍历完,将剩下的newChildren的节点标记为Placement,即插入的Tag- Если newChildren и oldFiber не были пройдены, введите логику перемещения узла
-
третий обход
Основная логика в функции placeChild, например порядок узлов до обновления ABCD, а после обновления ACDB
-
A в первой позиции newChild и A в первой позиции oldFiber, один и тот же ключ можно использовать повторно, lastPlacedIndex=0
-
C во второй позиции newChild и B во второй позиции oldFiber, ключи разные и выскакивают из первого цикла, сохраняем BCD в oldFiber в карту
-
C во второй позиции в newChild не нужно перемещать, если index=2 > lastPlacedIndex=0 в oldFiber, lastPlacedIndex=2
-
newChild третья позиция в индексе D oldFiber in = 3> lastPlacedIndex = 2 перемещать не нужно, lastPlacedIndex = 3
-
Индекс = 1
Смотрите на картинку более интуитивно
-
Например, порядок узлов до обновления — ABCD, а после обновления — DABC.
-
D в первой позиции newChild и A в первой позиции oldFiber, ключи разные и не могут использоваться повторно, сохраните ABCD в oldFiber на карте, lastPlacedIndex=0
-
Первая позиция D в newChild: index=3 в oldFiber> lastPlacedIndex=0 не нужно перемещать, lastPlacedIndex=3
-
A во второй позиции в newChild имеет index=0
-
B на третьей позиции в newChild index=1
-
C на четвертой позиции в newChild index=2
Смотрите на картинку более интуитивно
код показывает, как показано ниже:
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; } -