нажмитеВойдите в репозиторий отладки исходного кода React.
ПредыдущийВзгляните на принцип вычислительного состояния React.После этого разберем процесс Diff.
После того, как React вычислит updateQueue на файбере, файбер имеет новое состояние, то есть состояние.Для компонентов класса состояние используется в функции рендеринга.Поскольку новое состояние получено, то Непосредственный приоритет выполнить рендеринг, чтобы получить ReactElement, который содержит новое состояние.
Предполагая, что рендер получает большое количество ReactElements через один раз, и если в этих ReactElements есть только несколько узлов, которые нужно обновить, то очевидно, что не все они могут быть обновлены, в это время необходим процесс diff. чтобы определить, какие узлы действительно нуждаются в обновлении.
структура исходного кода
В качестве примера возьмем компонент класса, вычисление состояния происходит в файберном узле beginWork, соответствующем компоненту класса.updateClassInstanceВ функции после завершения расчета состояния следует корректировкаfinishClassComponentВыполните diff и отметьте effectTag (то есть новую версию флага).
Маркировка effectTag может указать, как изменилось волокно, например: размещение, обновление, удаление.Эти помеченные волокна будут собраны на этапе завершения для формирования связанного списка effectList, только волокна, которые содержат эти операции, окончательно обновляются на этапе фиксации .
function updateClassComponent(
current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {
...
// 计算状态
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderLanes,
);
...
// 执行render,进入diff,为fiber打上effectTag
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderLanes,
);
return nextUnitOfWork;
}
существуетfinishClassComponentфункция, звонокreconcileChildFibersсделать diff, в то время какreconcileChildFibersфактическиChildReconciler, что является основной функцией diff,
Эта функция вызывает различные функции для обработки типов новых узлов, сгенерированных рендерингом компонента.
function ChildReconciler(shouldTrackSideEffects) {
...
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
// 单节点diff
}
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
// 多节点diff
}
...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any, lanes: Lanes,
): Fiber | null {
const isObject = typeof newChild === 'object' && newChild !== null;
if (isObject) {
// 处理单节点
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(
reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
lanes,
),
);
case REACT_PORTAL_TYPE:
...
case REACT_LAZY_TYPE:
...
}
}
if (typeof newChild === 'string' || typeof newChild === 'number') {
// 处理文本节点
}
if (isArray(newChild)) {
// 处理多节点
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
lanes,
);
}
...
}
return reconcileChildFibers;
}
Тело Диффа
Что касается участников Diff, то это видно во входных параметрах функции reconcileChildren.
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
- workInProgress: передается как родительский узел, на него будет указывать возврат только что сгенерированного первого волокна.
- current.child: Старый узел волокна, когда diff генерирует новый узел волокна, он будет сравниваться с вновь сгенерированным ReactElement.
- nextChildren: вновь сгенерированный ReactElement создаст на его основе новый узел волокна.
- renderLanes: приоритет рендеринга на этот раз в конечном итоге будет установлен в свойстве lanes нового волокна.
Видно, что две предметы Diff: OldFiber (Current.child) и NewChildren (NextChildren, новая реакция), которые являются двумя различными структурами данных.
Например, сейчас есть компонент.После того, как он вычислит новое состояние, ему нужно сделать diff на основе этих двух вещей, которыеВ существующем дереве волокна (текущее дерево)
current树中
<Example/> fiber
|
|
A --sibling---> B --sibling---> C
Результат выполнения функции рендеринга
current fiber 对应的组件render的结果
[
{$$typeof: Symbol(react.element), type: "div", key: "A" },
{$$typeof: Symbol(react.element), type: "div", key: "B" },
{$$typeof: Symbol(react.element), type: "div", key: "B" },
]
Основные принципы Диффа
Для старой и новой структур в сцене есть три ситуации: обновление узла, добавление и удаление узла и перемещение узла. Перед лицом сложных ситуаций даже самые передовые алгоритмы оказываются чрезвычайно сложными. Столкнувшись с этой ситуацией, React реагирует следующими стратегиями:
- Даже если поддеревья двух элементов абсолютно одинаковы, но родительские элементы до и после разные, согласно правилам, элемент div и его поддерево будут полностью уничтожены, а элемент ap и его поддерево будут перестроены, и не будет предпринято никаких попыток повторного использования поддерева.
旧
<div>
<span>a</span>
<span>b</span>
</div>
新
<p>
<span>a</span>
<span>b</span>
</p>
- Используйте тег (имя тега) и ключ для идентификации узлов и определения того, были ли изменены узлы до и после, чтобы как можно больше повторно использовать неизмененные узлы.
旧
<p key="a">aa</p>
<h1 key="b">bb</h1>
新
<h1 key="b">bb</h1>
<p key="a">aa</p>
Из-за существования тега и ключа React может знать, что эти два узла меняются только в позиции.
Сцены
Как упоминалось выше, алгоритм сравнения работает с тремя сценариями:节点更新、节点增删、节点移动, но дочерним элементом волокна может быть один узел или несколько узлов. Поэтому в соответствии с этими двумя типами узлов его можно дополнительно подразделить на:
- Обновление одного узла, добавление и удаление одного узла.
- Обновление нескольких узлов, добавление и удаление нескольких узлов, перемещение нескольких узлов.
Что такое обновление узла? Для узлов DOM, когда тип узла (тег) и ключ одинаковы до и после, атрибуты узла изменились, что является обновлением узла. Если теги или ключи узлов до и после различаются, алгоритм Diff будет считать, что новый узел не имеет ничего общего со старым узлом.
В следующем примере имя className нового узла с ключом b изменилось, что является обновлением узла.
旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'b'}>bb</div>
В следующем примере, хотя именно имя нового узла изменилось, ключ также изменился, что не обновление узла
旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<div className={'bcd'} key={'bbb'}>bb</div>
В следующем примере, несмотря на то, что имя className нового узла изменилось, тег также изменился, что не является частью обновления узла.
旧
<div className={'a'} key={'a'}>aa</div>
<div className={'b'} key={'b'}>bb</div>
新
<div className={'a'} key={'a'}>aa</div>
<p className={'bcd'} key={'b'}>bb</p>
Ниже приводится отдельное описание соответствующих стратегий обновления для одноузловых и многоузловых систем.
один узел
Если элемент, созданный компонентом, имеет следующий тип:
<div key="a">aa</div>
Затем ReactElement, который он наконец создает, выглядит следующим образом (опуская некоторые атрибуты, не связанные с diff):
{
$$typeof: Symbol(react.element), type: "div", key: "a"
...
}
Один узел означает, что newChildren является одним узлом, но число oldFiber не обязательно, поэтому на самом деле возможны следующие три сценария:
Чтобы снизить затраты на понимание, мы проиллюстрируем проблему упрощенной моделью узлов, где буквы обозначают ключи.
- один старый узел
旧: A
新: A
- несколько старых узлов
旧: A - B - C
新: B
- нет старых узлов
旧: --
新: A
Фактически, для одноузлового diff выполняется только операция обновления, и никаких изменений в смещении и положении не происходит.reconcileSingleElementобработка функций. Эта функция охватывает три вышеупомянутых сценария. Но на самом деле для React есть только две ситуации, пуста ли цепочка oldFiber. Поэтому в реализации обрабатываются только эти два случая.
Цепочка oldFiber не пуста
Пройдите их, найдите узел с тем же ключом, затем удалите оставшиеся узлы oldFiber, а затем используйте свойства нового узла в соответствующих oldFiber и newChildren для создания нового узла волокна.
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
if (child.key === key) {
switch (child.tag) {
case Fragment:
...
case Block:
...
default: {
if (child.elementType === element.type) {
// 先删除剩下的oldFiber节点
deleteRemainingChildren(returnFiber, child.sibling);
// 基于oldFiber节点和新节点的props新建新的fiber节点
const existing = useFiber(child, element.props);
existing.ref = coerceRef(returnFiber, child, element);
existing.return = returnFiber; return existing;
}
break;
}
}
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 没匹配到说明新的fiber节点无法从oldFiber节点新建
// 删除掉所有oldFiber节点
deleteChild(returnFiber, child);
}
child = child.sibling;
}
...
}
цепочка oldFiber пуста
Если узла oldFiber нет, можно создать только узел newFiber. Логика не сложная.
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes
): Fiber {
const key = element.key;
let child = currentFirstChild;
while (child !== null) {
// oldFiber链非空的处理
...
} if (element.type === REACT_FRAGMENT_TYPE) {
// 处理Fragment类型的节点
...
} else {
// 用产生的ReactElement新建一个fiber节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
Обновление одного узла — это такой процесс, а реальная сложная ситуация — это различие нескольких узлов. Потому что это включает в себя добавление, удаление и перемещение узлов.
многоузловой
Если элемент DOM, окончательно созданный компонентом, выглядит следующим образом:
<div key="a">aa</div>
<div key="b">bb</div>
<div key="c">cc</div>
<div key="d">dd</div>
Тогда окончательный newChildren выглядит следующим образом (без учета некоторых атрибутов, не связанных с diff)
[
{$$typeof: Symbol(react.element), type: "div", key: "a" },
{$$typeof: Symbol(react.element), type: "div", key: "b" },
{$$typeof: Symbol(react.element), type: "div", key: "c" },
{$$typeof: Symbol(react.element), type: "div", key: "d" }
]
Существуют следующие четыре возможности для многоузловых изменений.
- Обновление узла
旧: A - B - C
新: `A - B - C`
- новый узел
旧: A - B - C
新: A - B - C - `D - E`
- удалить узел
旧: A - B - C - `D - E`
新: A - B - C
- Движение узла
旧: A - B - C - D - E
新: A - B - `D - C - E`
Многоузловой случай должен быть любой комбинацией этих четырех случаев, которая вызоветreconcileChildrenArrayСделайте разл. Согласно приведенным выше четырем случаям, он будет выполнять до трех раундов обхода с newChildren в качестве основного тела, но эти три раунда обхода не являются независимыми друг от друга.На самом деле, только первый раунд начинается с начала, и каждый последующий раунд заканчивается предыдущим раундом.Точка останова продолжается. На самом деле, в обычной практике больше всего обновляется сам узел, поэтому алгоритм Diff отдаст приоритет обновленному узлу. Таким образом, четырехраундовый обход можно разделить на две части в зависимости от сцены:
Первый раунд заключается в обновлении атрибутов самого узла, а оставшиеся два раунда связаны с добавлением и перемещением узлов по очереди, и основное внимание уделяется обработке движущихся узлов, поэтому в этой статье основное внимание будет уделено обработке узлов. обновление и движение узлов.Добавлен простой ремень.
Обновление узла
В первом раунде newChildren просматривается с самого начала, и он будет сравниваться с узлами в цепочке oldFiber один за другим, чтобы определить, изменился ли ключ или тег узла.
- Если изменений нет, клонируйте узел волокна, реквизиты которого обновляются из узла oldFiber, а новые реквизиты поступают из нового узла в newChildren, реализуя таким образом обновление узла.
- Если есть изменение, то это означает, что условия мультиплексирования не выполняются, и немедленно прервать обход и перейти к следующему обходу. Благодаря этой операции также значительно снижается сложность алгоритма Diff.
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
...
// 更新节点,对于DOM节点来说,updateSlot内部会判断
// key 和 tag。任意一个不同,则返回null
const newFiber = updateSlot( returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber为null则说明当前的节点不是更新的场景,中止这一轮循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
...
}
Давайте рассмотрим пример, предполагая, что старый и новый узлы выглядят следующим образом:
Старый: А - Б -C - D-Э
Новый: А - Б -D - C
В этом раунде обхода он пройдетA - B - D - C. И A, и B являются узлами с неизмененными ключами, которые можно использовать повторно напрямую, однако при переходе к D обнаруживается, что ключ изменился, и текущий обход выпрыгивает. В примере A и B — это узлы, которые обновляют себя.В следующих D и C мы видим, что его положение изменилось относительно цепочки oldFiber, и он спустится в цикл обработки мобильных узлов.
Ссылки на движущиеся узлы
Для удобства описания узлы, которые остаются на месте, называются фиксированными узлами. После обработки этого цикла видно, что фиксированными узлами являются A и B. В newChildren положение самого правого фиксированного узла очень важно, и его значение состоит в том, чтобы обеспечить опорную позицию для последующей обработки мобильных узлов. Таким образом, всякий раз, когда обрабатывается последний фиксированный узел, запоминайте его положение в это время, т. е.lastPlacedIndex. Код ключа следующий:
let newIdx = 0;
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
...
// 跳出逻辑
...
// 如果不跳出,记录最新的固定节点的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
...}
placeChildМетод на самом деле является методом перемещения узла, но когда узел не нужно перемещать, он вернет позицию текущего узла.Для фиксированного узла, поскольку нет необходимости перемещать, индекс фиксированного узла узел возвращается.
Удаление узла
Мы не упоминали об обработке удаленных узлов, на самом деле удалить узлы относительно просто.
Старый: А - В - С -D - EНовое: А-В-С
Поскольку обход является newChildren, когда обход заканчивается, но цепочка oldFiber не была пройдена, оставшиеся узлы должны быть удалены. Непосредственно отметьте effectTag удаления на узле oldFiber, чтобы добиться удаления.
if (newIdx === newChildren.length) {
// 新子节点遍历完,说明剩下的oldFiber都是没用的了,可以删除
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
deleteRemainingChildrenназываетсяdeleteChild, Стоит отметить, что удаление не только помечает EffectTag как Delete, но также добавляет удаленный узел волокна в список эффектов родителя.
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
...
const last = returnFiber.lastEffect;
// 将要删除的child添加到父级fiber的effectList中,并添加上effectTag为删除
if (last !== null) {
last.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
childToDelete.nextEffect = null;
childToDelete.effectTag = Deletion;
}
новый узел
Сценарий добавления новых узлов также понятен: когда цепочка oldFiber пройдена, а newChildren не пройдена, оставшиеся узлы принадлежат вновь вставленным узлам, а новый узел волокна будет создан и подключен для формирования цепочки. цепочка волокон с родным братом в качестве указателя.
Старый: А-В-С
Новый: А - В - С -D - E
Логика вставки (опустить код, который не очень актуален)
if (oldFiber === null) {
// 旧的遍历完了,意味着剩下的都是新增的了
for (; newIdx < newChildren.length; newIdx++) { // 首先创建newFiber
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
...
// 再将newFiber连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
Движение узла
Движение узлов происходит следующим образом:
старый А - Б -C - D - E - F
Новый А-Б-D - C - E
После первого раунда обработки обхода фиксированным узлом является A B, а положение последнего фиксированного узла (lastPlacedIndex) равно 1 (положение B). В этот момент C-D-E-F остается в цепочке oldFiber, а D-C-E остается в newChildren.
Следующая логика будет обновлять, а затем перемещать узлы с разными позициями. Поскольку в это время положение оставшихся узлов изменилось, и узлы oldFiber должны быть повторно использованы в обновлении, чтобы облегчить поиск во время обновления, оставшиеся узлы oldFiber будут помещены в карту с ключом в качестве ключа. и значение узла oldFiber. называетсяexistingChildren.
Поскольку узлы newChildren и oldFiber не были пройдены, значит, позицию нужно переместить. В этот момент должно быть ясно, чтоВсе эти узлы находятся справа от последнего фиксированного узла..
Логика хода такова: оставшиеся узлы в newChildren не уверены двигаться или нет, проходят их, и каждый идет смотреть позицию (старую позицию) этого узла в цепочке oldFiber, а у пройденного узла она есть in newChildren Расположение в (новое местоположение):
Если старая позиция находится в lastPlacedIndexправильно, что указывает на то, что положение этого узла остается неизменным.
Причина в том, что старая позиция находится в lastPlacedIndexправильно, и расположение нового узла также находится в егоправильно, поэтому его положение не меняется. Поскольку позиция не меняется, она становится фиксированным узлом, а lastPlacedIndex обновляется до новой позиции.
Если старая позиция находится слева от lastPlacedIndex, текущая позиция этого узла должна быть перемещена вправо.
Причина в том, что старая позиция находится в lastPlacedIndexосталось, новая позиция - lastPlacedIndexправильно, поэтому он идет вправо, но это не фиксированный узел. На данный момент нет необходимости обновлять lastPlacedIndex.
Давайте пройдемся по этой части логики на приведенном выше примере.
старый А - Б -C - D - E - FНовый А-Б-D - C - E
Позиция фиксированной части A - B, крайний правый фиксированный узел - B, lastPlacedIndex -1. В это время оставшаяся цепочка oldFiber — это C — D — E — F, а существующие дочерние элементы — это
{
C: '节点C',
D: '节点D',
E: '节点E',
F: '节点F'
}
Остальные части newChildren D - C - E продолжают траверс.
Первый проход к D, позиция D в цепочке oldFiber (A-B-C-D-E) равна 3
3 > 1, позиция D в oldFiber находится справа от B, и то же самое верно в newChildren, поэтому позиция D не меняется, и последний фиксированный узел становитсяD, обновите lastPlacedIndex до3. и удалить D из существующих детей,
{
C: '节点C',
E: '节点E',
F: '节点F'
}
Затем перейдите к C, индекс C в цепочке oldFiber (A - B - C - D - E) равен 2
2 < 3, C оказывается в последнем фиксированном узле (D), C в newChildren находится вDвправо, поэтому переместите его вправо. и удалите C из существующих дочерних элементов.
{
E: '节点E',
F: '节点F'
}
Затем перейдите к E, позиция E в цепочке oldFiber (A - B - C - D - E) равна 4
4 > 3, позиция E в цепочке oldFiberDСправа от позиции , то же верно и в новой позиции, поэтому позиция E не перемещается, а последний фиксированный узел становитсяE, обновите lastPlacedIndex до4. и удалите E из существующих детей,
{
F: '节点F'
}
В это время все newChildren обрабатываются, и обход мобильного узла заканчивается. На данный момент остается один F-узел, который находится в цепочке oldFiber.Поскольку newChildren обработан, его можно удалить.
existingChildren.forEach(child => deleteChild(returnFiber, child));
Можно видеть, что движение узла основано на фиксированном положении узла в крайнем правом углу в качестве ссылки. Эти фиксированные узлы являются узлами, положение которых не изменилось. Очень важно своевременно обновлять фиксированный узел после каждого сравнения того, нужно ли перемещать узел.
исходный код
После понимания описанного выше принципа многоузлового сравнения удобнее сопоставить приведенные выше ключевые моменты с исходным кодом для дальнейшего понимания. Исходный код с подробными комментариями приведен ниже.
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
/* * returnFiber:currentFirstChild的父级fiber节点
* currentFirstChild:当前执行更新任务的WIP(fiber)节点
* newChildren:组件的render方法渲染出的新的ReactElement节点
* lanes:优先级相关
* */
// resultingFirstChild是diff之后的新fiber链表的第一个fiber。
let resultingFirstChild: Fiber | null = null;
// resultingFirstChild是新链表的第一个fiber。
// previousNewFiber用来将后续的新fiber接到第一个fiber之后
let previousNewFiber: Fiber | null = null;
// oldFiber节点,新的child节点会和它进行比较
let oldFiber = currentFirstChild;
// 存储固定节点的位置
let lastPlacedIndex = 0;
// 存储遍历到的新节点的索引
let newIdx = 0;
// 记录目前遍历到的oldFiber的下一个节点
let nextOldFiber = null;
// 该轮遍历来处理节点更新,依据节点是否可复用来决定是否中断遍历
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// newChildren遍历完了,oldFiber链没有遍历完,此时需要中断遍历
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber; oldFiber = null;
} else {
// 用nextOldFiber存储当前遍历到的oldFiber的下一个节点
nextOldFiber = oldFiber.sibling;
}
// 生成新的节点,判断key与tag是否相同就在updateSlot中
// 对DOM类型的元素来说,key 和 tag都相同才会复用oldFiber
// 并返回出去,否则返回null
const newFiber = updateSlot(
returnFiber,
oldFiber,
newChildren[newIdx],
lanes,
);
// newFiber为 null说明 key 或 tag 不同,节点不可复用,中断遍历
if (newFiber === null) {
if (oldFiber === null) {
// oldFiber 为null说明oldFiber此时也遍历完了
// 是以下场景,D为新增节点
// 旧 A - B - C
// 新 A - B - C - D oldFiber = nextOldFiber;
}
break;
}
if (shouldTrackSideEffects) {
// shouldTrackSideEffects 为true表示是更新过程
if (oldFiber && newFiber.alternate === null) {
// newFiber.alternate 等同于 oldFiber.alternate
// oldFiber为WIP节点,它的alternate 就是 current节点
// oldFiber存在,并且经过更新后的新fiber节点它还没有current节点,
// 说明更新后展现在屏幕上不会有current节点,而更新后WIP
// 节点会称为current节点,所以需要删除已有的WIP节点
deleteChild(returnFiber, oldFiber);
}
}
// 记录固定节点的位置
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新fiber连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
// 将oldFiber节点指向下一个,与newChildren的遍历同步移动
oldFiber = nextOldFiber;
}
// 处理节点删除。新子节点遍历完,说明剩下的oldFiber都是没用的了,可以删除.
if (newIdx === newChildren.length) {
// newChildren遍历结束,删除掉oldFiber链中的剩下的节点
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// 处理新增节点。旧的遍历完了,能复用的都复用了,所以意味着新的都是新插入的了
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
// 基于新生成的ReactElement创建新的Fiber节点
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 记录固定节点的位置lastPlacedIndex
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新生成的fiber节点连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
return resultingFirstChild;
}
// 执行到这是都没遍历完的情况,把剩余的旧子节点放入一个以key为键,值为oldFiber节点的map中
// 这样在基于oldFiber节点新建新的fiber节点时,可以通过key快速地找出oldFiber
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 节点移动
for (; newIdx < newChildren.length; newIdx++) {
// 基于map中的oldFiber节点来创建新fiber
const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, );
if (newFiber !== null) {
if (shouldTrackSideEffects) {
if (newFiber.alternate !== null) {
// 因为newChildren中剩余的节点有可能和oldFiber节点一样,只是位置换了,
// 但也有可能是是新增的.
// 如果newFiber的alternate不为空,则说明newFiber不是新增的。
// 也就说明着它是基于map中的oldFiber节点新建的,意味着oldFiber已经被使用了,所以需
// 要从map中删去oldFiber
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 移动节点,多节点diff的核心,这里真正会实现节点的移动
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将新fiber连接成以sibling为指针的单向链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
if (shouldTrackSideEffects) {
// 此时newChildren遍历完了,该移动的都移动了,那么删除剩下的oldFiber
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
return resultingFirstChild;
}
Суммировать
Алгоритм Diff выбирает узлы с помощью ключей и тегов, которые могут напрямую перехватывать сложные сравнения, а затем понижать их уровень до относительно простых операций, таких как перемещение узла, добавление и удаление. Сравнение между старым волокном и новым узлом ReactElement создаст новые узлы волокна, помеченные с помощью effectTag, и эти волокна будут подключены к дереву workInProgress как новые узлы WIP. Таким образом, структура дерева определяется по крупицам, и новый узел workInProgress в основном дорабатывается. Это означает, что после сравнения узел beginWork узла workInProgress завершен. Затем он войдет в стадию CompleteWork.