Погрузитесь глубоко в новую архитектуру React под названием Fiber и узнайте о двух основных фазах нового алгоритма согласования (reconciliation). Мы подробно рассмотрим, как React обновляет состояние и пропсы и обрабатывает дочерние элементы.


React - это JavaScript библиотека для создания пользовательских интерфейсов. В ее основе лежит механизм, который отслеживает изменения в состоянии компонента и проецирует обновленное состояние на экран. В React мы знаем этот процесс как согласование (reconciliation). Мы вызываем метод setState, фреймворк проверяет, изменилось ли состояние или пропс, и перерендеривает компонент в UI.

Документация React предоставляет хороший высокоуровневый обзор механизма: роль элементов React, методы жизненного цикла и метод render, а также алгоритм диффиринга (сравнения, diffing), применяемый к дочерним элементам компонента. Дерево иммутабельных элементов React, возвращаемых методом render, обычно называют "виртуальный DOM". Этот термин помог объяснить React людям в самом начале, но он также вызвал путаницу и больше не используется в документации по React. В этой статье я буду называть его деревом React-элементов.

Помимо дерева React-элементов, фреймворк всегда имел дерево внутренних экземпляров (компонентов, узлов DOM и т.д.), используемых для хранения состояния. Начиная с версии 16, React развернул новую реализацию этого дерева внутренних экземпляров и алгоритма, который управляет им, под кодовым названием Fiber. Чтобы узнать о преимуществах архитектуры Fiber, ознакомьтесь с The how and why on React's use of linked list in Fiber.

Это первая статья из цикла, цель которого - научить вас внутренней архитектуре React. В этой статье я хочу предоставить углубленный обзор важных концепций и структур данных, имеющих отношение к алгоритму. Как только мы получим достаточную базу, мы изучим алгоритм и основные функции, используемые для обхода и обработки fiber-дерева. В следующих статьях цикла будет показано, как React использует алгоритм для выполнения начального рендеринга и обработки обновлений состояния и пропсов. Далее мы перейдем к деталям планировщика, процессу согласования (reconciliation) дочерних элементов и механизму построения списка эффектов.

Здесь я собираюсь дать вам довольно продвинутые знания. Я рекомендую вам прочитать ее, чтобы понять магию, скрывающуюся за внутренними механизмами Concurrent React. Эта серия статей также послужит вам отличным руководством, если вы планируете начать вносить свой вклад в React. Я сильно верю в реверс-инжиниринг, поэтому здесь будет много ссылок на исходники недавней версии 16.6.0.

Это, безусловно, довольно много, так что не переживайте, если вы не поймете что-то сразу. Это займет время, как и все стоящее. Обратите внимание, что вам не нужно знать ничего из этого, чтобы использовать React. Эта статья о том, как React работает внутри.

Базовый бэкграунд

Вот простое приложение, которое я буду использовать на протяжении всей серии. У нас есть кнопка, которая просто увеличивает число, отображаемое на экране:

А вот и реализация:

class ClickCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }
    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }

    render() {
        return [
            <button key="1" onClick={this.handleClick}>Update counter</button>,
            <span key="2">{this.state.count}</span>;
        ]
    }
}

Вы можете поиграть с ним здесь. Как вы можете видеть, это простой компонент, который возвращает два дочерних элемента button и span из метода render. Как только вы нажимаете на кнопку, состояние компонента обновляется внутри обработчика. Это, в свою очередь, приводит к обновлению текста элемента span.

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

  • обновляет свойство count в state в ClickCounter.

  • извлекает и сравнивает дочерние элементы ClickCounter и их пропсы

  • обновляет пропсы элемента span

Есть и другие действия, выполняемые во время согласования, такие как вызов методов жизненного цикла или обновление refsВсе эти действия в совокупности называются "работой" (work) в архитектуре Fiber. Тип работы обычно зависит от типа элемента React. Например, для классового компонента React должен создать экземпляр, в то время как для функционального компонента он этого не делает. Как вы знаете, в React есть много видов элементов, например, классовые и функциональные компоненты, компоненты-хосты (DOM узлы), порталы и т. д. Тип элемента React определяется первым параметром функции createElement. Эта функция обычно используется в методе render для создания элемента.

Прежде чем приступить к изучению действий и основного fiber алгоритма, давайте сначала познакомимся со структурами данных, используемыми внутри React.

От React-элементов к Fiber-узлам

Каждый компонент в React имеет UI-представление, которое мы можем назвать представлением или шаблоном, возвращаемым методом render. Вот шаблон для нашего компонента ClickCounter:

<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>

React-элементы

Когда шаблон проходит через JSX-компилятор, в итоге вы получаете набор React-элементов. Это то, что действительно возвращается из метода render компонентов React, а не HTML. Поскольку от нас не требуется обязательно использовать JSX, метод render для нашего компонента ClickCounter можно переписать следующим образом:

class ClickCounter {
    ...
    render() {
        return [
            React.createElement(
                'button',
                {
                    key: '1',
                    onClick: this.onClick
                },
                'Update counter'
            ),
            React.createElement(
                'span',
                {
                    key: '2'
                },
                this.state.count
            )
        ]
    }
}

Вызовы React.createElement в методе render создадут две структуры данных:

[
    {
        $$typeof: Symbol(react.element),
        type: 'button',
        key: "1",
        props: {
            children: 'Update counter',
            onClick: () => { ... }
        }
    },
    {
        $$typeof: Symbol(react.element),
        type: 'span',
        key: "2",
        props: {
            children: 0
        }
    }
]

Вы можете видеть, что React добавляет свойство $$typeof к этим объектам, чтобы однозначно идентифицировать их как React элементы. Затем у нас есть свойства type, key и props, которые описывают элемент. Значения берутся из того, что вы передаете в функцию React.createElement. Обратите внимание, как React представляет текстовое содержимое в качестве дочерних элементов узлов span и button. А обработчик клика является частью пропсов элемента button. Существуют и другие поля в элементах React, например, поле ref, которые выходят за рамки этой статьи.

У элемента React созданного для ClickCounter нет ни пропсов, ни ключа:

{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ClickCounter
}

Fiber-узлы

Во время согласования (reconciliation) данные каждого React элемента, возвращенные из метода render, объединяются в дерево fiber-узлов. Каждый React элемент имеет соответствующий fiber-узел. В отличие от React элементов, fibers не создаются заново при каждом рендере. Это мутабельные структуры данных, которые хранят состояние компонентов и DOM.

Ранее мы обсуждали, что в зависимости от типа React элемента фреймворк должен выполнять различные действия. В нашем примере приложения для классового компонента ClickCounter он вызывает методы жизненного цикла и метод render, в то время как для хост-компонента span (DOM узел) он выполняет мутацию DOM. Таким образом, каждый элемент React преобразуется в Fiber-узел соответствующего типа, который описывает работу, которую необходимо выполнить.

Вы можете думать о Fiber как о структуре данных, которая представляет некоторую работу, которую нужно выполнить, или, другими словами, единицу работы. Архитектура Fiber также обеспечивает удобный способ отслеживания, планирования, приостановки и прерывания работы.

Когда элемент React впервые преобразуется в fiber-узел, React использует данные из элемента для создания fiber в функции createFiberFromTypeAndProps. При последующих обновлениях React повторно использует fiber-узел и просто обновляет необходимые свойства, используя данные из соответствующего React-элемента. React также может потребоваться переместить узел в иерархии на основе пропсов key или удалить его, если соответствующий элемент React больше не возвращается из метода render.

Посмотрите функцию ChildReconciler, чтобы увидеть список всех действий и соответствующих функций, которые React выполняет для существующих fiber-узлов.

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

Все fiber-узлы связаны между собой через связный список (linked list), используя следующие свойства fiber-узлов: child, sibling и return. Более подробно о том, почему это работает именно так, читайте в моей статье The how and why on React's use of linked list in Fiber, если вы еще не читали ее.

Текущее (current) дерево и work in progress дерево

После первого рендеринга React заканчивает работу с fiber-деревом, которое отражает состояние приложения, использованного для отрисовки пользовательского интерфейса. Это дерево часто называют текущим (current). Когда React начинает работать над обновлениями, он строит так называемое workInProgress дерево, которое отражает будущее состояние, которое будет выведено на экран.

Вся работа выполняется над fibers из дерева workInProgress. Когда React проходит через текущее дерево, для каждого существующего fiber-узла он создает альтернативный узел, который составляет workInProgressдерево. Этот узел создается с использованием данных из элементов React, возвращаемых методом render. Как только обновления будут обработаны и вся связанная с ними работа будет завершена, React получит альтернативное дерево, готовое к выводу на экран. Как только это workInProgress дерево будет выведено на экран, оно станет текущим деревом.

Один из основных принципов React - последовательность. React всегда обновляет DOM за один раз - он не показывает частичные результаты. Дерево workInProgress служит в качестве "черновика", который не виден пользователю, чтобы React мог сначала обработать все компоненты, а затем вывести их изменения на экран.

В исходных текстах вы увидите множество функций, которые берут fiber-узлы из обоих деревьев current и workInProgress. Вот сигнатура одной из таких функций:

function updateHostComponent(current, workInProgress, renderExpirationTime) {...}

Каждый fiber-узел содержит ссылку на свой аналог из другого дерева в поле alternate. Узел из текущего дерева указывает на узел из дерева workInProgress и наоборот.

Побочные эффекты (Side-effects)

Мы можем думать о компоненте в React как о функции, которая использует состояние и пропсы для вычисления UI-представления. Любые другие действия, такие как мутирование DOM или вызов методов жизненного цикла, следует рассматривать как побочный эффект или, просто, эффект. Эффекты также упоминаются в документации:

Вероятно, вам уже приходилось выполнять выборку данных, подписку или вручную изменять DOM из React компонентов. Мы называем эти операции "побочными эффектами" (или сокращенно "эффектами"), потому что они могут повлиять на другие компоненты и не могут быть выполнены во время рендеринга.

Вы можете видеть, как большинство обновлений состояния и пропсов приводят к побочным эффектам. А поскольку применение эффектов - это один из видов работы, узел fiber - это удобный механизм для отслеживания эффектов в дополнение к обновлениям. Каждый узел волокна может иметь эффекты, связанные с ним. Они кодируются в поле effectTag.

Таким образом, эффекты в Fiber в основном определяют работу, которую необходимо выполнить для экземпляров после обработки обновлений. Для хост компонентов (DOM элементов) работа заключается в добавлении, обновлении или удалении элементов. Для классовых компонентов React может потребоваться обновление refs и вызов методов жизненного цикла componentDidMount и componentDidUpdate. Существуют также другие эффекты, соответствующие другим типам fiber-ов.

Список эффектов (effects list)

React обрабатывает обновления очень быстро, и для достижения такого уровня производительности он использует несколько интересных приемов. Один из них - построение линейного списка fiber-узлов с эффектами для быстрой итерации. Итерация линейного списка намного быстрее, чем дерева, и нет необходимости тратить время на узлы без побочных эффектов.

Цель этого списка - пометить узлы, которые имеют DOM обновления или другие эффекты, связанные с ними. Этот список является подмножеством дерева finishedWork и связан посредством свойства nextEffect вместо свойства child, используемого в деревьях current и workInProgress.

Dan Abramov предложил аналогию для списка эффектов. Ему нравится думать о нем как о рождественской елке, с "рождественскими огнями", связывающими все узлы эффектов вместе. Чтобы визуализировать это, давайте представим следующее дерево из fiber-узлов, где выделенные узлы выполняют определенную работу. Например, в результате нашего обновления c2 был вставлен в DOM, d2 и c1 изменили атрибуты, а b2 запустил метод жизненного цикла. Список эффектов свяжет их вместе, чтобы React мог пропустить другие узлы позже:

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

Корень fiber-дерева

В каждом React-приложении есть один или несколько элементов DOM, которые выступают в качестве контейнеров. В нашем случае это элемент div с ID container.

const domContainer = document.querySelector('#container');
ReactDOM.render(React.createElement(ClickCounter), domContainer);

React создает объект fiber root для каждого из этих контейнеров. Вы можете получить к нему доступ, используя ссылку на DOM элемент:

const fiberRoot = query('#container')._reactRootContainer._internalRoot

Этот fiber root является местом, где React хранит ссылку на fiber tree. Она хранится в свойстве current fiber root:

const hostRootFiberNode = fiberRoot.current

Fiber-дерево начинается со специального типа fiber-узла, которым является HostRoot. Он создается внутри и действует как родитель для вашего самого верхнего компонента. Существует связь от fiber-узла HostRoot обратно к FiberRoot через свойство stateNode:

fiberRoot.current.stateNode === fiberRoot; // true

Вы можете изучить fiber-дерево, обратившись к самому верхнему fiber-узлу HostRoot через fiber root. Или вы можете получить отдельный fiber-узел из экземпляра компонента следующим образом:

compInstance._reactInternalFiber

Структура fiber-узла

Теперь рассмотрим структуру fiber-узлов, созданных для компонента ClickCounter:

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

и DOM-элемента 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
}

В fiber-узлах довольно много полей. Я описал назначение полей alternate, effectTag и nextEffect в предыдущих разделах. Теперь давайте посмотрим, зачем нам нужны другие.

stateNode

Хранит ссылку на экземпляр класса компонента, узла DOM или другой тип React элемента, связанный с fiber-узлом. В общем, можно сказать, что это свойство используется для хранения локального состояния, ассоциированного с fiber.

type

Определяет функцию или класс, связанный с этим fiber. Для классовых компонентов оно указывает на функцию-конструктор, а для DOM элементов - на HTML-тег. Я довольно часто использую это поле, чтобы понять, с каким элементом связан fiber-узел.

tag

Определяет тип fiber. Он используется в алгоритме согласования (reconciliation), чтобы определить, какую работу нужно выполнить. Как упоминалось ранее, работа варьируется в зависимости от типа React элемента. Функция createFiberFromTypeAndProps сопоставляет элемент React с соответствующим типом fiber-узла. В нашем приложении свойство tag для компонента ClickCounter равно 1, что обозначает ClassComponent, а для элемента span - 5, что обозначает HostComponent.

updateQueue

Очередь обновлений состояния, обратных вызовов и обновлений DOM.

memoizedState

Состояние fiber-a, которое было использовано для создания вывода. При обработке обновлений оно отражает состояние, которое в данный момент выводится на экран.

memoizedProps

Пропсы fiber-а, которые были использованы для создания вывода во время предыдущего рендера.

pendingProps

Пропсы, которые были обновлены на основе новых данных в React элементах и должны быть применены к дочерним компонентам или DOM элементам.

key

Уникальный идентификатор с группой дочерних элементов, помогающий React выяснить, какие элементы изменились, были добавлены или удалены из списка. Это связано с функциональностью "списки и ключи" React, описанным здесь.

Полную структуру fiber-узла можно найти здесь. В приведенном выше объяснении я пропустил кучу полей. В частности, я пропустил указатели child, sibling и return, составляющие древовидную структуру данных, которую я описал в своей предыдущей статье. И пропустил категорию полей, таких как expirationTime, childExpirationTime и mode, которые специфичны для Scheduler.

Итоговый алгоритм

React выполняет работу в двух основных фазах: render и commit.

Во время первой фазы render React применяет обновления к компонентам, запланированные через setState или React.render, и выясняет, что нужно обновить в пользовательском интерфейсе. Если это первоначальный рендеринг, React создает новый fiber-узел для каждого элемента, возвращенного из метода render. При последующих обновлениях fiber-ы для существующих React элементов используются повторно и обновляются. Результатом фазы является дерево fiber-узлов, помеченных побочными эффектами. Эффекты описывают работу, которая должна быть выполнена во время следующей фазы commit. Во время этой фазы React берет fiber-дерево, помеченное эффектами, и применяет их к экземплярам. Он просматривает список эффектов и выполняет обновления DOM и другие изменения, видимые пользователю.

Важно понимать, что работа во время первой render фазы может выполняться асинхронно. React может обработать один или несколько fiber-узлов в зависимости от доступного времени, затем остановиться, чтобы сохранить проделанную работу и уступить какому-либо событию. Затем он продолжает работу с того места, где остановился. Иногда, однако, может потребоваться отбросить проделанную работу и начать все сначала. Такие паузы возможны благодаря тому, что работа, выполняемая в этой фазе, не приводит к каким-либо видимым пользователю изменениям, таким как обновление DOM. В отличие от этого, следующая commit фаза всегда синхронна. Это происходит потому, что работа, выполняемая на этом этапе, приводит к изменениям, видимым пользователю, например, к обновлению DOM. Поэтому React должен выполнять её за один проход.

Вызов методов жизненного цикла - это один из видов работы, выполняемой React. Некоторые методы вызываются на этапе render, а другие - на этапе commit. Вот список методов жизненного цикла, вызываемых при выполнении первой фазы render:

  • [UNSAFE_]componentWillMount (deprecated)

  • [UNSAFE_]componentWillReceiveProps (deprecated)

  • getDerivedStateFromProps

  • shouldComponentUpdate

  • [UNSAFE_]componentWillUpdate (deprecated)

  • render

Как вы можете видеть, некоторые устаревшие методы жизненного цикла, выполняемые на этапе render, помечены как UNSAFE с версии 16.3. В документации они теперь называются legacy lifecycles. Они будут устаревшими в будущих релизах 16.x, а их аналоги без префикса UNSAFE будут удалены в версии 17.0. Подробнее об этих изменениях и предлагаемом пути миграции вы можете прочитать здесь.

Вам интересно узнать причину этого?

Ну, мы только что узнали, что поскольку фаза render не производит побочных эффектов, таких как обновление DOM, React может обрабатывать обновления компонентов асинхронно (потенциально даже делая это в несколько потоков).Однако жизненные циклы, помеченные UNSAFE, часто понимались неправильно и часто использовались не по назначению. Разработчики склонны помещать код с побочными эффектами внутрь этих методов, что может вызвать проблемы с новым подходом к асинхронному рендерингу. Хотя будут удалены только их аналоги без префикса UNSAFE, они все еще могут вызвать проблемы в предстоящем Concurrent Mode (от которого вы можете отказаться).

Вот список методов жизненного цикла, выполняемых во время второй фазы commit:

  • getSnapshotBeforeUpdate

  • componentDidMount

  • componentDidUpdate

  • componentWillUnmount

Поскольку эти методы выполняются в синхронной фазе commit, они могут содержать побочные эффекты и затрагивать DOM.

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

Render фаза

Алгоритм согласования (reconciliation) всегда начинается с самого верхнего fiber-узла HostRoot с помощью функции renderRoot. Например, если вы вызовете setState глубоко в дереве компонентов, React начнет с вершины, но быстро пропустит родительские узлы, пока не доберется до компонента, у которого был вызван метод setState.

Основные этапы рабочего цикла

Все fiber-узлы обрабатываются в рабочем цикле (work loop). Вот реализация синхронной части цикла:

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {...}
}

В приведенном выше коде переменная nextUnitOfWork хранит ссылку на fiber-узел из дерева workInProgress, в котором еще есть незавершенная работа. Когда React обходит fiber-дерево, он использует эту переменную, чтобы узнать, есть ли еще какой-нибудь fiber-узел с незавершенной работой. После обработки текущего fiber переменная будет содержать либо ссылку на следующий fiber-узел в дереве, либо null. В этом случае React выходит из рабочего цикла и готов закоммитить изменения.

Существует 4 основные функции, которые используются для обхода дерева и инициирования или завершения работы:

Чтобы продемонстрировать, как они используются, посмотрите на следующую анимацию обхода fiber-дерева. Для демонстрации я использовал упрощенную реализацию этих функций. Каждая функция берет для обработки fiber-узел, и по мере того, как React спускается по дереву, вы можете видеть, как меняется текущий активный fiber-узел. На анимации хорошо видно, как алгоритм переходит от одной ветви к другой. Сначала он завершает работу для дочерних узлов, а затем переходит к родительским.

Обратите внимание, что прямые вертикальные связи обозначают сиблингов (братьев и сестер), а изогнутые - детей, например, у b1 нет детей, а у b2 есть один ребенок c1.

Вот ссылка на видео, где можно приостановить воспроизведение и просмотреть текущий узел и состояние функций. Концептуально, вы можете думать о "begin" как о "шаге в" компонент, а о "complete" как о "шаге из" него. Вы также можете поиграть с примером и реализацией здесь, пока же я объясню, что делают эти функции.

Давайте начнем с первых двух функций performUnitOfWork и beginWork:

function performUnitOfWork(workInProgress) {
    let next = beginWork(workInProgress);
    if (next === null) {
        next = completeUnitOfWork(workInProgress);
    }
    return next;
}

function beginWork(workInProgress) {
  console.log('work performed for ' + workInProgress.name);
  return workInProgress.child;
}

Функция performUnitOfWork получает fiber-узел из дерева workInProgress и начинает работу, вызывая функцию beginWork. Это функция, которая запускает все действия, которые должны быть выполнены для fiber-а. Для целей данной демонстрации мы просто записываем в лог имя fiber-а, чтобы обозначить, что работа была выполнена. Функция beginWork всегда возвращает указатель на следующего ребенка для обработки в цикле или null.

Если есть следующий ребенок, он будет присвоен переменной nextUnitOfWork в функции workLoop. Однако если дочернего элемента нет, React знает, что достиг конца ветви, и поэтому может завершить текущий узел. После того, как узел будет завершен, нужно будет выполнить работу для сиблингов и вернуться к родителю после этого. Это делается в функции completeUnitOfWork:

function completeUnitOfWork(workInProgress) {
    while (true) {
        let returnFiber = workInProgress.return;
        let siblingFiber = workInProgress.sibling;
        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            // Если существует сиблинг, возвращаем его для выполнения работы для него
            return siblingFiber;
        } else if (returnFiber !== null) {
            // Если больше нет работы в этом returnFiber,
            // продолжаем цикл для завершения родителя
            workInProgress = returnFiber;
            continue;
        } else {
            // мы достигли корня
            return null;
        }
    }
}

function completeWork(workInProgress) {
  console.log('work completed for ' + workInProgress.name);
  return null;
}

Вы можете видеть, что весь код функции заключается в большом цикле while. React попадает в эту функцию, когда у узла workInProgress нет дочерних элементов. После завершения работы для текущего fiber-а, он проверяет, есть ли у него дочерний узел. Если он найден, React выходит из функции и возвращает указатель на сиблинга. Он будет присвоен переменной nextUnitOfWork, и React выполнит работу для ветви, начинающейся с этого сиблинга. Важно понимать, что на данный момент React выполнил работу только для предшествующих сиблингов. Он не выполнил работу для родительского узла. Только когда все ветви, начинающиеся с дочерних узлов, завершены, он завершает работу для родительского узла и возвращается назад..

Как видно из реализации, обе функции и completeUnitOfWork используются в основном для целей итерации, в то время как основная деятельность происходит в функциях beginWork и completeWork. В следующих статьях цикла мы узнаем, что происходит для компонента ClickCounter и узла span, когда React переходит к функциям beginWork и completeWork.

Commit фаза

Фаза начинается с функции completeRoot. Именно здесь React обновляет DOM и вызывает методы жизненного цикла до и после мутации.

Когда React доходит до этой фазы, у него есть 2 дерева и список эффектов. Первое дерево представляет состояние, которое в настоящее время отображается на экране. Затем есть альтернативное дерево, построенное во время фазы render. Оно называется finishedWork или workInProgress в источниках и представляет состояние, которое должно быть отражено на экране. Это альтернативное дерево связано с текущим деревом через указатели child и sibling.

И затем, есть список эффектов - подмножество узлов из дерева finishedWork, связанное через указатель nextEffect. Помните, что список эффектов - это результат выполнения фазы render. Весь смысл рендеринга в том, чтобы определить, какие узлы должны быть вставлены, обновлены или удалены, и какие компоненты должны вызвать свои методы жизненного цикла. И именно об этом нам говорит список эффектов. И это именно тот набор узлов, который итерируется во время фазы commit.

В целях отладки доступ к текущему дереву можно получить через свойство current fiber-корня. Доступ к дереву finishedWork можно получить через свойство alternate узла HostFiber в текущем дереве.

Основной функцией, выполняемой на этапе commit, является commitRoot. В основном, она делает следующее:

  • Вызывает метод жизненного цикла getSnapshotBeforeUpdate на узлах, помеченных эффектом Snapshot.

  • Вызывает метод componentWillUnmount жизненного цикла для узлов, помеченных эффектом Deletion.

  • Выполняет все вставки, обновления и удаления в DOM

  • Устанавливает дерево finishedWork в качестве текущего.

  • Вызывает метод жизненного цикла componentDidMount на узлах, помеченных эффектом Placement.

  • Вызывает метод componentDidUpdate жизненного цикла для узлов, помеченных эффектом Update.

После вызова метода предварительной мутации getSnapshotBeforeUpdate, React фиксирует все побочные эффекты внутри дерева. Он делает это в два прохода. Первый проход выполняет все вставки, обновления, удаления и размонтирования DOM (хоста). Затем React назначает дерево finishedWork на FiberRoot, помечая дерево workInProgress как current. Это делается после первого прохода фазы commit, чтобы предыдущее дерево было актуальным во время componentWillUnmount, но до второго прохода, чтобы законченная работа была актуальной во время componentDidMount/Update. Во время второго прохода React вызывает все остальные методы жизненного цикла и обратные вызовы. Эти методы выполняются как отдельный проход, так что все размещения, обновления и удаления во всем дереве уже были вызваны.

Вот сниппет функции, которая выполняет описанные выше шаги:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}

Каждая из этих подфункций реализует цикл, который итерирует список эффектов и проверяет их тип. Когда он находит эффект, соответствующий цели функции, он применяет его.

Pre-mutation методы жизненного цикла

Вот, например, код, который выполняет итерацию по дереву эффектов и проверяет, имеет ли узел эффект Snapshot:

function commitBeforeMutationLifecycles() {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & Snapshot) {
            const current = nextEffect.alternate;
            commitBeforeMutationLifeCycles(current, nextEffect);
        }
        nextEffect = nextEffect.nextEffect;
    }
}

Для классового компонента это действие означает вызов метода жизненного цикла getSnapshotBeforeUpdate.

Обновления DOM

commitAllHostEffects - это функция, с помощью которой React выполняет обновление DOM. Функция в основном определяет тип операции, которую необходимо выполнить для узла, и выполняет ее:

function commitAllHostEffects() {
    switch (primaryEffectTag) {
        case Placement: {
            commitPlacement(nextEffect);
            ...
        }
        case PlacementAndUpdate: {
            commitPlacement(nextEffect);
            commitWork(current, nextEffect);
            ...
        }
        case Update: {
            commitWork(current, nextEffect);
            ...
        }
        case Deletion: {
            commitDeletion(nextEffect);
            ...
        }
    }
}

Интересно, что React вызывает метод componentWillUnmount как часть процесса удаления в функции commitDeletion.

Post-mutation методы жизненного цикла

commitAllLifecycles - это функция, в которой React вызывает все оставшиеся методы жизненного цикла componentDidUpdate и componentDidMount.


Наконец-то мы закончили. Дайте мне знать, что вы думаете о статье или задайте вопросы в комментариях. Посмотрите следующую статью из цикла Глубокое объяснение обновления состояния и пропсов в React. У меня в работе еще много статей, в которых подробно рассказывается о планировщике, процессе согласования детей и о том, как строится список эффектов. Я также планирую создать видео, в котором покажу, как отлаживать приложение, используя эту статью в качестве основы.

Комментарии (3)


  1. nin-jin
    24.04.2022 01:34
    -1


    1. ssurrokk Автор
      24.04.2022 10:07

      начал изучать технологию А, стоп погоди, смотри есть крутая технология Б. Ок, начал изучать технологию Б, стой, ты что, смотри есть крутая технология В. И т.д. Вывод - нужно сначала хоть что-то хорошо изучить, и лучше чтобы это было что-то популярное


      1. nin-jin
        25.04.2022 17:14

        Лучше сначала подумать, сравнить, а потом уже глубоко изучать.