Слегка запеченный основной механизм React: React Fiber и примирение

React.js

React Fiber — это новая архитектура React v16.x, а Reconciliation — это алгоритм React Diff, оба из которых являются основным механизмом React. В этой статье мы изучим React Fiber и Reconciliation, а также узнаем, что такое Fiber? Как согласование работает на оптоволокне? Как работает алгоритм согласования? Поскольку это основной механизм React, он включает в себя множество концепций и логики, брат-барбекю может только «слегка» поджарить его, обобщить некоторые основные принципы, а детали реализации на уровне исходного кода нуждаются в дальнейшем изучении (и a Объем этой статьи, безусловно, не является четким или исчерпывающим).

Статья слишком длинная, рекомендуется посмотреть после сбора

1. От DOM к объекту волокна

Давайте начнем с «закуски», давайте посмотрим на связанные концепции DOM, Virtual DOM, элементы React и объекты Fiber.

ДОМ:

Модель объекта документа, фактически дерево, каждый узел в дереве представляет собой HTML-элемент (элемент), каждый узел на самом деле является объектом JS, в дополнение к некоторым атрибутам, содержащим элемент, а также предоставляет некоторые интерфейсы (методы), так что программирование Язык может быть вставлен и управляется DOM. Но сам дом не оптимизирован для веб-приложения для динамического интерфейса. Поэтому, когда страница должна быть обновлена, обновленный DOM сделает программу медленнее. Поскольку браузер должен сделать все стили и элементы HTML. Это на самом деле часто происходит, когда нет никаких изменений на странице.

Виртуальный дом:

Чтобы оптимизировать обновление «реального» DOM, была выведена концепция «Виртуальный DOM». По сути, виртуальный DOM — это симуляция реального DOM, который на самом деле является деревом. Настоящее DOM-дерево состоит из реальных DOM-элементов, а виртуальное DOM-дерево состоит из виртуальных DOM-элементов.

Когда состояние компонента React изменится, будет сгенерировано новое дерево Virtual DOM, а затем React будет использовать алгоритм сравнения для сравнения нового и старого деревьев Virtual DOM, получить разницу, а затем обновить «разницу» до реальной. одно дерево DOM для завершения обновления пользовательского интерфейса.

Чтобы уточнить, это не означает, что использование Virtual DOM может ускорить операции DOM, а означает, что React использует как можно меньше операций DOM, когда страница переходит из одного состояния в другое.

Реагировать элемент:

В мире React React называет элементы Virtual DOM React Elements. То есть дерево Virtual DOM в React состоит из элементов React, и каждый узел в дереве является элементом React.

Давайте посмотрим на определения типов элементов React из исходного кода (/package/shared/ReactElementType.js):

export type Source = {|
  fileName: string,
  lineNumber: number,
|};

export type ReactElement = {|
  $$typeof: any,
  type: any,
  key: any,
  ref: any,
  props: any,
  // ReactFiber
  _owner: any,
  ...
|};
  • $$typeof: логотип элемента React типа Symbol;
  • type: тип элемента React. Если это пользовательский компонент (составной компонент), то значением поля type является класс или функциональный компонент; если это собственный HTML (хост-компонент), если это div, span и т. д., то значение типа — строка ('div', 'span');
  • key: ключ элемента React, который используется при выполнении алгоритма сравнения;
  • ref: Атрибут ref элемента React, когда элемент React становится настоящим DOM, возвращает ссылку на настоящий DOM;
  • props: свойство элемента React, который является объектом;
  • _owner: Компонент, ответственный за создание этого элемента React, и из комментария «// ReactFiber» в коде мы можем узнать, что он содержит экземпляр объекта волокна, связанного с элементом React;

Когда мы пишем компонент React, будь то компонент класса или компонент функции, возвращенный JSX будет скомпилирован компилятором JSX (компилятором JSX).В процессе компиляции он вызоветReact.createElement()Сюда.

при звонкеReact.createElement()При фактическом вызове ReactElement.js (/packages/react/src/ReactElement.js)createElement()метод. После вызова этого метода создается элемент React.

В приведенном выше примере компонента ClickCounter<button>а также<span>да<ClickCounter>подкомпонент . а также<ClickCounter>组件其实它本身其实也是一个组件。 это<App>Подкомпоненты компонентов:

class App extends React.Component {
    ...
    render() {
        return [
            <ClickCounter />
        ]
    }
}

Так зовет<App>изrender(), создаст<ClickCounter>Элемент реакции, соответствующий компоненту:

при исполненииReactDOM.render()После этого все созданное дерево React Rlement выглядит примерно так:

Волоконные объекты:

Всякий раз, когда мы создаем элемент реакции, мы также создаем узел волокна, связанный с этим элементом реакции. узел волокна является экземпляром объекта волокна.

Объект Fiber — это структура данных, используемая для сохранения «состояния компонента», «соответствующей DOM-информации компонента» и «рабочей задачи (работы)», отвечающая за управление обновлением экземпляров компонентов, задач рендеринга и отношения с другими узлами Fiber. С каждым компонентом (элементом реакции) связан соответствующий экземпляр объекта Fiber (узел волокна).В отличие от элемента реакции, узел волокна не нужно создавать заново каждый раз при обновлении интерфейса.

Во время выполнения алгоритма согласования информация (свойства) элемента реакции, возвращаемая методом рендеринга компонента, будет объединена с соответствующим узлом волокна. Таким образом, эти узлы волокна образуют дерево узлов волокна, соответствующее дереву элементов реакции. (Что мы должны иметь в виду:Каждый элемент реакции будет иметь соответствующий узел волокна.).

Определение типа объекта волокна (/package/react-reciler/src/reactinternaltypes.js):

export type Fiber = {|
    tag: WorkTag;
    key: null | string;
    type: any;
    stateNode: any;
    updateQueue: mixed;
    memoizedState: any;
    memoizedProps: any,
    pendingProps: any;
    nextEffect: Fiber | null,
    firstEffect: Fiber | null,
    lastEffect: Fiber | null,
    return: Fiber | null;
    child: Fiber | null;
    sibling: Fiber | null;
    ...
|};
  • tag: Это поле определяет тип оптоволоконного узла. В алгоритме согласования он используется для определения того, какую работу должен выполнять волоконный узел;
  • key: это поле имеет то же значение и содержание, что и ключ реагирующего элемента (поскольку этот ключ напрямую копируется и назначается из ключа реагирующего элемента), как уникальный идентификатор каждого элемента в дочернем списке. Он используется, чтобы помочь React выяснить, какой элемент был изменен, какой элемент был добавлен, а какой элемент был удален. В официальной документации есть более подробное объяснение ключа;
  • type: Это поле указывает тип элемента реакции, связанного с этим узлом волокна. Значение этого поля совпадает со значением поля типа элемента реакции (поскольку этот тип напрямую копируется и назначается из типа элемента реакции). Если это пользовательский компонент (составной компонент), то значением поля type является класс или функциональный компонент; если это собственный HTML (хост-компонент), такой как div, span и т. д., то значение type это строка ('div', 'span');
  • updateQueue: Это поле используется для хранения очереди задач обновления статуса компонента, обратного вызова и обновления DOM.Именно через это поле файбер-нода управляет задачами рендеринга и обновления реагирующего элемента, соответствующего файбер-ноде;(если старые утюги видел барбекю брат тот«Выпечка через React Hook», вы будете знать, что updateQueue на самом деле хранит связанный список эффектов, которые необходимо обработать узлу волокна);
  • memoizedState: состояние, которое было обновлено до реального DOM (состояние, которое было отображено в интерфейсе пользовательского интерфейса);
  • memoizedProps: реквизиты, которые были обновлены до реального DOM (реквизиты, которые были отображены в интерфейсе пользовательского интерфейса),
  • pendingProps: реквизиты, ожидающие обновления до настоящего DOM;
  • return: это поле эквивалентно указателю на родительский узел волокна;
  • child: это поле эквивалентно указателю на дочерний узел волокна;
  • sibling: Это поле эквивалентно указателю на родственный узел волокна;
  • nextEffect: указать на следующий узел волокна с побочным эффектом;
  • firstEffect: указывает на первый узел волокна с побочным эффектом
  • lastEffect: указывает на последний узел волокна с побочным эффектом;

Для синтаксического анализа других свойств см. комментарии в исходном коде илиэта статья.

Давайте посмотрим на пример выше<ClickCounter>Как выглядит узел волокна компонента:

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedState: {count: 0},
    pendingProps: {},
    memoizedProps: {},
    tag: 1,
    effectTag: 0,
    nextEffect: null
}

<span>Узел волокна:

{
    stateNode: new HTMLSpanElement,
    type: "span",
    alternate: null,
    key: "2",
    updateQueue: null,
    memoizedState: null,
    pendingProps: {children: 0},
    memoizedProps: {children: 0},
    tag: 5,
    effectTag: 0,
    nextEffect: null
}

выше<ClickCounter>В нашем примере, поскольку элемент реакции каждого компонента будет иметь соответствующий узел волокна, мы получим дерево узлов волокна:

Что такое побочные эффекты?

В определении типа узла волокна есть три свойства: firstEffect, lastEffect и nextEffect, которые указывают на узел волокна с «побочными эффектами». Так что же такое «побочный эффект»? Любой, кто писал компоненты React, знает, что компонент React на самом деле является функцией, которая получает свойства и состояние в качестве входных данных, а затем возвращает элемент реакции посредством вычислений. В этом процессе будут выполняться некоторые операции, такие как изменение структуры DOM, вызов жизненного цикла компонентов и т. д. React собирательно называет эти «операции» «побочными эффектами» или «эффектом» для краткости, т. е. часто называют «побочным эффектом».официальная документацияЕсть введение в эффекты.

Большинство обновлений состояния компонентов и свойств вызывают побочные эффекты. Кроме того, мы также можем настроить некоторые эффекты с помощью хука useEffect React. Написано перед барбекю«Выпечка через React Hook»Как упоминалось в статье, эффект узла волокна будет храниться в виде «кругового связанного списка», а затем updateQueue узла волокна будет указывать на круговой связанный список этого эффекта.

Список эффектов:

В дереве узлов волокна некоторые узлы волокна имеют эффекты для обработки, а некоторые узлы волокна не имеют эффектов для обработки. Чтобы ускорить обработку эффектов для всего дерева узлов волокна, React строит связанный список узлов волокна с эффектами, которые необходимо обработать, Этот связанный список называется «список эффектов». В этом связанном списке хранятся узлы волокна с эффектами. Причина сохранения этого списка такова: поскольку обход связанного списка выполняется намного быстрее, чем обход всего дерева волоконных узлов, нам не нужно тратить время на повторение узлов волокон, которые не имеют эффектов для обработки. Этот связанный список поддерживается свойствами firstEffect, lastEffect и nextEffect узла волокна, упомянутыми ранее: firstEffect указывает на первый узел волокна с эффектом, lastEffect указывает на последний узел волокна с эффектом, nextEffect указывает на следующий узел волокна с эффектом. волоконный узел эффекта.

Например, ниже представлено дерево узлов волокон, где выделенные узлы имеют узлы волокон, которые необходимо обработать эффекту. Предположим, что наш процесс обновления приведет к вставке H в DOM, C и D изменят свои атрибуты, G вызовет свои собственные методы жизненного цикла и т. д.

Затем список эффектов этого дерева узлов волокна соединит эти узлы вместе, чтобы React мог пропустить другие узлы волокна, у которых нет задач для обработки при обходе дерева узлов волокна.

резюме:

Давайте еще раз рассмотрим пошаговое абстрактное преобразование пользовательского интерфейса React на странице:

2. Реагирующая архитектура

1,Планировщик заданий Планировщик: Определить приоритет задачи рендеринга (обновления) и отдать приоритет задаче качественного обновления Реконсилеру; когда компонент класса вызывает метод render() (или возвращается компонент-функция), он фактически не запускает рендеринг этого компонента немедленно. , в это время он будет возвращать только «информацию о рендеринге» (описание того, что рендерить), которая содержит компоненты React, написанные пользователем (например,<MyComponent>), а также компоненты для конкретных платформ (такие как<div>). Затем React будет использовать планировщик, чтобы принять решение о выполнении задачи рендеринга компонента в какой-то момент в будущем.

2,Координатор по согласованию: Отвечает за поиск «различия» между двумя деревьями Virtual DOM (React Element) до и после и сообщает «разницу» визуализатору. Что касается того, как работает координатор, мы подробно изучим его позже;

3.Рендерер: Отвечает за обновление «разницы» с реальным DOM для обновления пользовательского интерфейса; разные платформы имеют разные средства визуализации. DOM — это лишь одна из платформ рендеринга, к которой может адаптироваться React. Другими основными платформами рендеринга являются уровень представления для IOS и Android (через средство рендеринга React Native). Этот разделенный дизайн означает, что React DOM и React Native могут использовать отдельные средства визуализации, используя при этом один и тот же согласователь, предоставляемый ядром React. React Fiber переписывает согласователь, но это в значительной степени не связано с рендерингом. Однако в любом случае многим рендерерам обязательно потребуются некоторые корректировки для интеграции с новой архитектурой.

3. Что такое примирение?

React — это библиотека JavaScript для создания пользовательских интерфейсов. Основной механизм React заключается в отслеживании изменений состояния компонентов и последующем сопоставлении обновленного состояния с пользовательским интерфейсом.

При использовании React роль функции render() в компоненте заключается в создании дерева элементов реакции. При вызове setState(), то есть при следующем обновлении состояния или реквизита, функция render() вернет другое дерево элементов реакции. Затем React будет использовать алгоритм Diff для эффективного обновления пользовательского интерфейса, чтобы он соответствовал самому последнему дереву элементов React. Этот алгоритм Diff является алгоритмом согласования.

Алгоритм согласования в основном делает две вещи:

  1. Найдите разницу между двумя деревьями реагирующих элементов;
  2. Обновите разницу до реального DOM, чтобы завершить обновление пользовательского интерфейса;

Stack Reconciler

В React 15.x и предыдущих версиях алгоритм согласования был реализован с помощью согласователя стека, но у согласователя стека в этот период есть некоторые дефекты: он не может приостанавливать задачи рендеринга, не может разделять задачи и не может эффективно балансировать компоненты. Обновить порядок выполнения задачи, связанные с рендерингом и анимацией, то есть задачи не могут иметь приоритет (это может вызвать такие проблемы, как зависание важных задач, пропуск кадров анимации и т. д.).Реализация Stack Reconciler.

Fiber Reconciler

Чтобы решить проблемы, присущие Stack Reconciler, и некоторые исторические проблемы, в React 16 был представлен новый алгоритм согласования под названием Fiber Reconciler, который заменит согласователь стека. Fiber Reconciler будет использовать планировщик для обработки рендеринга/обновления компонентов. Кроме того, после введения концепции волокна исходное дерево элементов реакции имеет соответствующее дерево узлов волокна. При различии различий между двумя деревьями реагирующих элементов Fiber Reconciler будет использовать алгоритм сравнения, основанный на дереве узлов волокна, и может более легко пройти дерево узлов волокна через возвращаемые, дочерние и одноуровневые свойства узла волокна, тем самым завершая алгоритм diff более эффективен.

Возможности Fiber Reconciler (Преимущества)

  1. Возможность нарезать прерываемые задачи;
  2. Возможность корректировать приоритеты задач, сбрасывать и повторно использовать задачи;
  3. Вы можете переключать задачи вперед и назад между задачами родительского и дочернего компонентов;
  4. Метод рендеринга может возвращать несколько элементов (то есть он может возвращать массив);
  5. Исключение обработки границы исключения поддержки;

4. Рабочий процесс сверки

Как упоминалось выше, алгоритм согласования в основном делает две вещи:

  1. Найдите разницу между двумя деревьями реагирующих элементов;
  2. Обновите разницу до реального DOM, чтобы завершить обновление пользовательского интерфейса;

Далее мы сосредоточимся на вышеупомянутых «двух вещах», чтобы увидеть, как работает алгоритм согласования.

1. Найдите разницу между двумя деревьями реагирующих элементов

три стратегии

Сравнивая два дерева элементов реакции, React сформулировал 3 стратегии:

  1. Сравнивайте только реагирующие элементы одного уровня. Если узел DOM пересекает иерархию между двумя обновлениями, React не будет пытаться использовать его повторно;
  2. Два элемента реакции разных типов (с разными полями типа) будут генерировать разные деревья элементов реакции. Например, если элемент div становится p, React уничтожит div и его дочерние узлы и создаст новый p и его дочерние узлы;
  3. Разработчики могут использовать ключевой атрибут, чтобы указать, какие дочерние элементы стабильны при различных визуализациях. Давайте используем пример, чтобы проиллюстрировать роль ключа:
// 更新前
<div>
    <p key="qianduan">前端</p>
    <h3 key="shaokaotan">烧烤摊</h3>
</div>
// 更新后
<div>
    <h3 key="shaokaotan">烧烤摊</h3>
    <p key="qianduan">前端</p>
</div>

Если ключа нет, React будет думать, что первый узел div изменен с p на h3, а второй дочерний узел изменен с h3 на p. Это соответствует принципу 2, поэтому соответствующий DOM-узел уничтожается и создается.

Но когда мы добавляем ключевой атрибут, указывается соответствующая связь между узлами.React знает, что p с ключом «shaokao» все еще существует после обновления, поэтому узлы DOM можно использовать повторно, но порядок нужно поменять местами.

Конкретный процесс Diff

(источник о Diff: /packages/react-reconciler/src/ReactChildFiber.new.js)

В соответствии с первой стратегией, описанной выше, «сравнивайте только реагирующие элементы одного уровня», что означает, что сравниваются только узлы одного уровня. Что значит "один и тот же класс"? - Те узлы, которые непосредственно принадлежат одному и тому же родительскому узлу, являются родственными узлами. Например:

Как показано на рисунке выше, корневые узлы старого и нового деревьев по умолчанию являются узлами одного уровня:

  1. Родительским узлом узлов B, C и D старого дерева элементов реакции является A;
  2. Родительскими узлами узлов B, C и D нового дерева элементов реакции также является A;
  3. Таким образом, узлы B, C и D старого дерева реагирующих элементов и узлы B, C и D нового дерева реагирующих элементов принадлежат к одному и тому же уровню. В процессе Diff будут сравниваться различия между B, C и D нового и старого дерева элементов реакции;
  4. Точно так же родительские узлы узлов E и F старого и нового деревьев являются узлами B, поэтому узлы E и F нового и старого деревьев являются узлами одного уровня;

При сравнении узла его можно разделить на две ситуации:

  1. Если один из типов или ключей (если ключа нет, посмотрите индекс) старой и новой ноды (элемента реакции) отличается, DOM не будет повторно использоваться и будет уничтожен напрямую;
  2. Если тип и ключ (если ключа нет, посмотрите индекс) старой и новой ноды (элемента реакции) совпадают, DOM будет использоваться повторно;
Сравните реагирующие элементы разных типов

Когда тип узла (элемента реакции) отличается от сравниваемого, React уничтожит исходный узел и его потомков, а затем воссоздаст новый узел и его потомков. Например:

// 旧
<div>
    <A />
    <div>
        <B />
        <C />
    </div>
</div>
// 新
<div>
    <A />
    <span>
        <B />
        <C />
    </span>
</div>

В приведенном выше примере исходный тип узла — div, а после обновления тип узла становится span.После того, как React находит разницу, он уничтожает узел div и его дочерние узлы (B и C), а затем воссоздает тип узла Узел пролета и его дочерние элементы (B и C).

Сравните реагирующие элементы одного типа (Композитный компонент, Хост-компонент)

Когда тип узла (элемента реакции) совпадает со сравнением, React сохранит узел DOM, соответствующий элементу реакции (повторно использует DOM), а затем только сравнит и обновит измененный атрибут (атрибут). Например:

// 旧
<div className="before" title="stuff" />
// 新
<div className="after" title="stuff" />

Для сравнения, React знает, что ему нужно только изменить атрибут className в элементе DOM.

При обновлении свойства стиля React обновляет только измененное свойство, например:

// 旧
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新
<div style={{color: 'green', fontWeight: 'bold'}} />

Напротив, React знает, что ему нужно изменить только цветовой стиль элемента DOM, а не fontWeight.

Приведенные выше примеры — это все Host Component (нативные HTML-элементы).Если сравнить однотипный Composite Component (React-компонент, написанный вами самостоятельно), главное на данный момент — изменились ли пропсы и состояние компонента. является изменением. Компонент и его подкомпоненты обновляются.

При сравнении узлов одного уровня (элементов реакции) необходимо учитывать две ситуации:

  1. На одном уровне есть только один узел (например, G на рисунке выше);
  2. На одном уровне есть несколько узлов (например, B, C, D на рисунке выше);
Существует только один узел на одном уровне

Эта ситуация относительно проста, нужно просто сравнить старые и новые узлы, и ее можно оценить и обработать в соответствии с двумя ситуациями, упомянутыми выше (типы узлов одинаковы, но типы разные).

Есть несколько узлов на одном уровне

При наличии нескольких узлов на одном уровне необходимо обрабатывать 3 ситуации:

  1. Обновление узла (тип, обновление атрибута)
  2. Добавление или удаление узла
  3. Мобильный узел местоположения

Для Diff с несколькими узлами на одном уровне он должен принадлежать к одному или нескольким из трех вышеперечисленных случаев. Команда React обнаружила, что при ежедневной разработке обновление компонентов происходит чаще, чем добавление и удаление, поэтому алгоритм React Diff будет расставлять приоритеты и обрабатывать обновления узлов.

Для нескольких узлов на одном уровне мы можем думать об этом как о связанном списке (поскольку на самом деле соответствующие узлы волокон реагирующих элементов на одном уровне будут подключены к односвязному списку через одноуровневое поле). Алгоритм Diff дважды пройдет по «новому списку одноуровневых узлов»:

  1. Первый раунд обхода: обработать обновленный узел (DOM, соответствующий узлу, можно использовать повторно, достаточно обновить некоторые его атрибуты);
  2. Второй раунд обхода: обработка новых, удаленных и перемещенных узлов;
первый круг обхода

(Для удобства следующее будет соответственно «старый узел сестры для дерева Element Element Element Element Tree» и «New Noide Element Element Free Three Three Sything», называемый «Список узлов Sibing» и «Новый список узлов брата».)

  1. Пройдите «связанный список новых одноранговых узлов» и «связанный список старых одноранговых узлов», пройдите от первого узла (i = 0) и определите, является ли тип (тип) нового и старого узлов одинаковым и является ли ключ совпадает, если тип и ключи совпадают, это означает, что соответствующий DOM можно использовать повторно;
  2. Если DOM, соответствующий этому узлу, можно использовать повторно, i++ оценит тип и ключ следующей группы новых и старых узлов, чтобы увидеть, можно ли повторно использовать их соответствующий DOM.Если его можно использовать повторно, повторите шаг 2;
  3. Если обнаруживается, что DOM, соответствующий определенной группе новых и старых узлов, не может быть повторно использован, обход завершается;
  4. Если пройден «связанный список новых одноранговых узлов» или пройден «связанный список старых одноранговых узлов», обход заканчивается;

Код простого моделирования описанного выше процесса выглядит следующим образом (обратите внимание, что это только код «простого моделирования», который отличается от конкретной реализации исходного кода. Если вы хотите увидеть конкретную реализацию исходного кода, пожалуйста, см. /packages/react-reconciler/srcReactChildFiber.new.jsreconcileChildArray()Функция):

// newNodeList 为 新同级节点链表
// oldNodeList 为 旧同级节点链表
for (let i = 0; i < newNodeList.length; i++) {
    if (!oldNodeList[i]) break;  // 如果「旧同级节点链表」已经遍历完了,则结束遍历
    if (newNodeList[i].key=== oldNodeList[i].key && 
        newNodeList[i].type === oldNodeList[i].type) {
        continue;  // 对应的 DOM 可复用,则继续遍历
    } else {
        break; // 对应的 DOM 不可复用,则结束遍历
    }
}

Для описанного выше процесса, когда мы пройдем конец, будет два результата:

«Результат один»: Обход завершен на шаге 3. В это время ни «новый список одноранговых узлов», ни «старый список одноранговых узлов» не были пройдены.

См. пример:

// 旧
<li key="0">0</li>
<li key="1">1</li>
<li key="2">2</li>
// 新
<li key="0">0</li>
<li key="1">1</li>
<div key="2">2</div>
<li key="3">3</li>

Предыдущие узлы с ключом === 0 и ключом === 1 можно использовать повторно, но когда ключ === 2, из-за изменения типа узла соответствующий DOM нельзя использовать повторно, и обход завершается напрямую. В это время узлы с ключом === 2 в «старом списке одноранговых узлов» не были пройдены, а узлы с ключом === 2 и ключом === 3 в «новом списке одноранговых узлов» не были пройдены. пройдено. .

«Результат два»: Если обход завершен на шаге 4, возможно, был пройден «связанный список новых одноранговых узлов», или «связанный список старых одноранговых узлов», или они были пройдены одновременно. Например:

// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
            
// 新
// 「新同级节点链表」和「旧同级节点链表」同时遍历完
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>

// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 新 
//「新同级节点链表」没遍历完,「旧同级节点链表」就遍历完了
<li key="0" className="aa">0</li>
<li key="1" className="bb">1</li>
<li key="2" className="cc">2</li>

// 旧
<li key="0" className="a">0</li>
<li key="1" className="b">1</li>
// 新
//「新同级节点链表」遍历完了,「旧同级节点链表」还没遍历完
<li key="0" className="aa">0</li>

Второй круг обхода

Во втором раунде обхода он в основном проходит через оставшиеся узлы в «новом списке одноранговых узлов», которые не были пройдены.

Если результат предыдущего обхода«Результат два»:

1. Если «новый список одноранговых узлов» не был пройден, а «старый список одноранговых узлов» был пройден, это означает, что добавляется новый узел, и новый узел будет помечен новым.Placementотметка (newFiber.flags = Placement).

2. Если «новый список одноранговых узлов» был пройден, а «старый список одноранговых узлов» не был пройден, это означает, что есть узлы, которые необходимо удалить, и удаляемый узел будет отмечен значком новый.Deletionотметка (returnFiber.flags |= Deletion).

Если результат предыдущего обхода«Результат один»:

Если это результат 1, это означает, что связанный список новых и старых одноранговых узлов не был пройден, а это означает, что некоторые узлы могли изменить свои позиции в этом обновлении! Далее идет узел, который обрабатывает преобразование положения. Две основные идеи для работы с преобразованиями положения узла:“剩下的节点中,哪些节点需要「右」移动? ","Переехать куда?".

Поскольку некоторые узлы поменялись местами, мы больше не можем сравнивать старые и новые узлы по их индексу. Не паникуйте, это не большая проблема, мы все еще можем использоватьkeyчтобы соответствовать старому и новому узлам.

При обходе «списка новых одноранговых узлов», чтобы быстро найти соответствующий старый узел в «списке старых одноранговых узлов», React не будет обрабатываться в «списке старых одноранговых узлов».mapФорма хранится, чтоkeyИмущество является ключевым,fiber nodeзначение, карта называетсяexistingChildren:

const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

existingChildrenКак это работает? Во время второго тура обхода:

1. Если ключ пройденного «нового однорангового узла» A находится вexistingChildrenможно найти в «списке старых одноранговых узлов», можно найти «старый одноранговый узел» A1 с тем же ключом, что и у A. Поскольку это соответствует реализации карты, ясно, что ключи A и A1 одинаковы, и следующим шагом будет определение того, совпадают ли их типы:

  • Если ключ тот же и тип тот же, это означает, что DOM, соответствующий узлу, можно использовать повторно, но позиция изменилась;
  • Если ключ тот же, а тип другой, DOM, соответствующий узлу, нельзя использовать повторно, исходный узел необходимо уничтожить, а новый узел необходимо повторно вставить;

2. Если ключ пройденного «нового однорангового узла» A находится вexistingChildrenЕсли он не найден в «списке старых одноранговых узлов», это означает, что «старый одноранговый узел» A1 с тем же ключом, что и у A, не может быть найден в «старом списке одноранговых узлов», что означает, что A является новым узлом. ;

После решения «как новые узлы соответствуют задаче поиска старых узлов». Далее давайте посмотрим, как работать с добавлением, удалением и перемещением узлов во втором цикле.

На самом деле ситуацию с добавлением и удалением узлов понять очень легко, ведь ситуация с добавлением и удалением была объяснена, когда выше упоминались «два результата». Далее мы сосредоточимся на ситуации движения узла. Как упоминалось ранее, имея дело с изменениями положения узлов, мы в основном улавливаем два момента:

  • Какой узел нужно сместить вправо?
  • Куда двигаться вправо?

Вышеупомянутые два вопроса на самом деле включаютнаправлениеа такжесмещение, если вы хотите прояснить эти две вещи, вам нужна "точка отсчета", или "точка отсчета". Реагировать используетlastPlacedIndexЭта переменная используется для хранения «точки отсчета». Мы можем найти его в исходном кодеreconcileChildrenArray()В начале функции см.:

let lastPlacedIndex = 0;

lastPlacedIndexЭта переменная представляет текущий последний повторно используемый узел, соответствующий индексу в «списке старых одноранговых узлов». Начальное значение равно 0. (Это определение может быть немного запутанным для понимания, но это не имеет значения, подождите и посмотрите два примера, чтобы узнать, что оно на самом деле содержит)

При обходе оставшегося «списка новых одноуровневых узлов» каждый новый узел будет проходитьexistingChildrenНайдите соответствующий старый узел, и тогда вы сможете получить индекс старого узла.oldIndex(то есть положение в «старом списке одноуровневых узлов»).

Далее будут вынесены следующие решения:

  • еслиoldIndex >= lastPlacedIndex, указывающий, что узлу мультиплексирования не нужно перемещать свою позицию, и установить lastPlacedIndex = oldIndex;
  • еслиoldIndex < lastPlacedIndex, что означает, что узел нужно переместить вправо, а узел нужно переместить в заднюю часть последнего пройденного нового узла;

Выше приведена логика обработки перемещения узла. После прочтения вы все еще можете быть немного сбиты с толку.В это время вам нужно взять его с каштанами, и эффект будет лучше~

Каштан 1:

Предположим, что есть два списка одноуровневых узлов, новые и старые (тип узлов, представленных всеми кружками на следующем рисунке, — li, а буква в кружке — это ключ узла):

// 旧
<li key="a">a</li>
<li key="b">b</li>
<li key="c">c</li>
<li key="d">d</li>
// 新
<li key="a">a</li>
<li key="c">c</li>
<li key="d">d</li>
<li key="b">b</li>

Первый - это первый раунд цикла:

Второй цикл:

Первый цикл только что обработал первый узел а. В настоящее время в «старом списке одноранговых узлов» еще есть еще b, c и d, которые не были пройдены, и c, d и d в «новом списке одноранговых узлов». .b еще не пройден. Обход связанных списков новых и старых одноранговых узлов не был завершен, то есть узлы не были добавлены или удалены, что указывает на то, что некоторые узлы изменили свои позиции. Следовательно, следующий второй цикл в основном связан с перемещением позиции узла. Перед началом обработки сначала сохраните непройденные узлы b, c и d в «старом списке одноранговых узлов» в виде карты.existingChildrenсередина.

«Список новых одноуровневых узлов» переходит к узлу c:

«Список новых одноуровневых узлов» переходит к узлу d:

«Список новых одноуровневых узлов» переходит к узлу b:

На этом второй раунд обхода заканчивается, в итоге ни один из DOM-узлов, соответствующих узлам a, c и d, не переместился, а DOM, соответствующий узлу b, будет помечен как «необходимо переместить».

Итак, после двух циклов React знает, что для перехода от «старого списка одноранговых узлов» к «новому списку одноранговых узлов» «старый список одноранговых узлов» должен пройти следующие операции каждого узла:

  1. Положение узла a остается неизменным;
  2. Узел b перемещается сразу за узлом d;
  3. Положение узла c остается неизменным;
  4. Положение узла d остается неизменным;

Какой? Чувствуете себя немного подавленным всего одним каштаном? Давай еще каштан~

Предполагая, что есть два списка одноуровневых узлов, старые и новые:

// 旧
<li key="a">a</li>
<li key="b">b</li>
<li key="c">c</li>
<li key="d">d</li>

// 新
<li key="d">d</li>
<li key="a">a</li>
<div key="b">b</div>
<li key="c">c</li>

Первый цикл:

Второй цикл:

«Список новых одноуровневых узлов» переходит к узлу d:

«Новая та же цепочка узлов», пройденная до узла a:

«Список новых одноуровневых узлов» переходит к узлу b:

«Список новых одноуровневых узлов» переходит к узлу c:

На этом второй круг обхода заканчивается.

После двух циклов React знает, что для перехода от «старого списка одноранговых узлов» к «новому списку одноранговых узлов» «старый список одноранговых узлов» должен пройти каждую из следующих операций:

  1. Положение узла d остается неизменным;
  2. Узел а перемещается сразу за узлом d;
  3. Вставьте новый узел b после узла a, тип которого равен div, и удалите исходный узел b;
  4. Узел c перемещается сразу за узлом b;

Соответствующая «работа» (работа) каждого из вышеперечисленных узлов — «куда переместить», «положение без изменений», «вставить новый, удалить старый» и т. д. — будет храниться в соответствующем файберном узле узла. Когда дело доходит до фазы рендеринга, React считывает и выполняет эти «операции» для завершения обновления DOM.

резюме:

Давайте рассмотрим весь процесс сравнения на следующей диаграмме:

2. Обновите разницу до реального DOM, чтобы завершить обновление пользовательского интерфейса.

Обнаружив «различия» с помощью приведенного выше сравнения, React знает, «какие элементы реакции нужно удалить», «какие элементы реакции должны добавить дочерние узлы», «какие позиции элементов реакции необходимо переместить», «какие элементы реакции атрибуты должны быть перемещены" Ряд операций, таких как "обновление" и т. д., эти операции будут рассматриваться как задачи обновления (работы). Собственная задача обновления (работа) каждого элемента реакции хранится в узле волокна, соответствующем этому элементу реакции.

существуетЭтап рендеринга, Согласование начнется с верхнего узла дерева узлов волокна и повторно обработает все дерево узлов волокна.обход в глубину, пройтись по каждому узлу волокна в дереве и обработать работу, хранящуюся в узле волокна. Процесс обхода дерева узлов волокна для выполнения в нем работы называетсяwork loop. Работа волоконного узла завершается, когда завершена работа над собой и всеми его дочерними ветвями. Как только работа узла волокна завершена, то есть узел волокна завершен, React приступит к обработке работы своего родственного узла (узел волокна, на который указывает поле silbing), после завершения работы узла волокна. родственный узел (sibling) После работы он продолжит переход к следующему родственному узлу... и так далее. Когда работа всех одноуровневых узлов будет обработана, React вернется к родительскому узлу (пошаговый возврат через поле возврата). Этот процесс происходит вcompleteUnitOfWork 函数(/packages/react-reconciler/src/ReactFiberWorkLoop.new.js).

Разработчики React сделали здесь оптимизацию («Список эффектов», упомянутую ранее): React будет пропускать те узлы волокна, которые уже были обработаны, и обрабатывать только те узлы волокна с незавершенной работой. Например, если вы вызываете глубоко в дереве компонентовsetState()метод, то React по-прежнему будет проходить от верхнего узла дерева узлов волокна, но пропустит все предыдущие родительские узлы и перейдет прямо к вызовуsetState()Дочерний узел метода.

когдаwork loopПосле окончания (то есть после обхода всего дерева узлов волокна) он будет готов к входуэтап фиксации. существуетэтап фиксации, React обновит реальное дерево DOM, чтобы завершить рендеринг обновления пользовательского интерфейса.

(PS: Из-за ограниченного места, о более конкретном процессе в фазе рендеринга и фазе фиксации, роли и изменении каждого поля узла волокна в этом процессе, а также подробности планировщика, рендерера и т. д. Я напишу еще несколько статей [смеется и плачет], и позже у меня будет возможность провести дальнейшее исследование и подвести итоги)

Пять, идеи просмотра исходного кода

(На основе исходного кода React v17.0.1)

  1. Определение типа ReactElement: package/shared/ReactElementType.js
  2. определение типа волокна: packages/react-reconciler/src/ReactInternalTypes.js
  3. Создайте волокно: packages/react-reconciler/src/ReactFiber.new.js
  4. процесс сравнения: функция reconcileChildFibers (/packages/react-reconciler/src/ReactChildFiber.new.js)
  5. Процесс рабочего цикла: функция completeUnitOfWork (/packages/react-reconciler/src/ReactFiberWorkLoop.new.js)

6. Постскриптум

Эта статья основана на исходном коде React и некоторых статьях в Интернете.Ввиду сложного внутреннего механизма React и ограниченных возможностей брата-барбекю в тексте могут быть ошибки или недостаточное резюме.Надеюсь, вы сможете закончить барбекю.Позже я указал в области комментариев, что все будут общаться и обсуждать вместе, с нетерпением ожидая углубления понимания передовых знаний посредством обмена с ветеранами.

7. Ссылки

Подпишитесь на общедоступную учетную запись Nuggets или WeChat «Front-end BBQ Stall» и получите сводку и выводы брата-барбекю как можно скорее.