От мелкой к глубокой архитектуре React Fiber

React.js

fiber-cover

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

  • Каковы болевые точки в React15? Что такое клетчатка? Почему React16 нужно представить Fiber?
  • Как реализовать виртуальный DOM под React16?
  • Как реализовать структуру данных и алгоритм обхода Fiber?
  • Как реализовать прерываемое и возобновляемое планирование задач в архитектуре Fiber?
    • Как указать количество для обновления? Как выполнить пакетное обновление?
  • Как реализовать рендеринг компонентов и отправку коллекции побочных эффектов в архитектуре Fiber?
  • Как реализовать стратегию оптимизации согласования и двойной буферизации в Fiber?
  • Как реализовать такие хуки, как useReducer и useState?
  • Как реализовать приоритет задачи expireTime, планирование задач и обработку тайм-аута?
  • Как реализовать оптимизированную обработку ключей для согласования domdiff?
  • Как реализовать синтетическое событие SyntheticEvent?
  • Как реализовать реф useEffect?

Эта статья первая@careteen/react, укажите источник для перепечатки. На складе хранятся все коды реализации и примеры, а желающие могут сделать форк для отладки.

содержание

Стратегия планирования React15

JavaScript похож на улицу с односторонним движением.

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

browser-render

Рендеринг React15 и diff будут рекурсивно сравниватьсяVirtualDOM树,Найдите узел, который был изменен, а затем обновите их синхронно.Весь процесс в одном идет. Так что, если количество узлов страниц очень большой,React

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

Чтобы реализовать параллелизм процессов, операционная система будет распределять права выполнения ЦП на несколько процессов в соответствии с определенной стратегией планирования, и несколько процессов имеют возможность выполняться, позволяя им выполняться поочередно, создавая иллюзию работает одновременно. Поскольку процессор такой быстрый, люди его вообще не чувствуют. Фактически в одноядерной физической среде одновременно работает только одна программа.

Стратегия планирования задач браузера и процесс рендеринга

pubg-stuck

Для игры требуется плавная частота обновления, которая составляет не менее 60 Гц. В противном случае игровой опыт оставляет желать лучшего.

Так что же содержит кадр?

a-frame

Средний кадр составляет 16,66 мс, который в основном делится на следующие части:

  • выполнение скрипта
  • расчет стиля
  • макет
  • перерисовать
  • синтез

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

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

После синтеза также空闲阶段, то есть если синтез и все предыдущие этапы трудоемки16.66ms, в остальное время браузер дает намrequestIdleCallbackЗвоните, в полной мере использовать их.

requestIdleCallbackВ настоящее время поддерживает только хром, нужноpolyfill

requestIdleCallback-api

Общий процесс выглядит следующим образом:

requestIdleCallback-flow

пример requestIdleCallback

requestIdleCallback позволяет разработчикам выполнять фоновую и низкоприоритетную работу в основном цикле событий, не влияя на критичные к задержке события, такие как анимация и ответы ввода.

Преимущества связанных списков

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

Подробнее о реализации и использовании связанных списков

макет setState

setState

Как и выше, вы можете использовать связанный список для достижения чего-то вродеReact的setState方法.

// 表示一个节点
class Update {
  constructor(payload, nextUpdate) {
    this.payload = payload
    this.nextUpdate = nextUpdate
  }
}

Узел нуждаетсяpayloadДанные монтирования,nextUpdateуказать на следующий узел.

// 模拟链表
class UpdateQueue {
  constructor() {
    this.baseState = null
    this.firstUpdate = null
    this.lastUpdate = null
  }
  enqueue(update) {
    if (!this.firstUpdate) {
      this.firstUpdate = this.lastUpdate = update
    } else {
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
}

Требуется при инициализации связанного спискаbaseStateхранить данные,firstUpdateуказывает на первый узел,lastUpdateуказывает на последний узел.

так же какenqueueСоедините узлы вместе.

const isFunction = (func) => {
  return typeof func === 'function'
}
class UpdateQueue {
  forceUpdate() {
    let currentState = this.baseState || {}
    let currentUpdate = this.firstUpdate
    while(currentUpdate) {
      const nextState = isFunction(currentUpdate.payload) ? currentUpdate.payload(currentState) : currentUpdate.payload
      currentState = {
        ...currentState,
        ...nextState
      }
      currentUpdate = currentUpdate.nextUpdate
    }
    this.firstUpdate = this.lastUpdate = null
    return this.baseState = currentState
  }
}

все еще требуетсяforceUpdateОбъединить данные, смонтированные на всех узлах. похожий наReact.setState()Параметры могут быть объектами или функциями.

Волоконная архитектура

Что делать перед клетчаткой

существуетReact15До и,Reactбудет рекурсивно сравниватьVirtualDOMдерева, найдите узлы, которые необходимо изменить, и обновите их синхронно. этот процессReactназываетсяReconciliation(协调).

существуетReconciliationпериод,ReactВсегда будет заниматься браузером ресурсов, запускаемые пользователем события не приведут к нему ответа, а во-вторых, он приведет к сброшенным кадрам, пользователь может чувствовать себя CATON. Мы смоделируем обход.

ДОМДИФФ из React15

dom-tree

Сопоставьте структуру узлов приведенного выше изображения с виртуальным DOM.

const root = {
  key: 'A1',
  children: [
    {
      key:  'B1',
      children: [
        {
          key: 'C1',
          children: []
        },
        {
          key: 'C2',
          children: []
        }
      ]
    },
    {
      key:  'B2',
      children: []
    }
  ]
}

Он проходится с использованием алгоритма поиска в глубину.

Подробно объясните DFS

function walk(vdom, cb) {
  cb && cb(vdom)
  vdom.children.forEach(child => walk(child, cb))
}
// Test
walk(root, (node) => {
  console.log(node.key) // A1 B1 C1 C2 B2
})

существуетDom-DiffТо же самое относится и к рекурсивному сравнению обхода, и есть две проблемы, которые очень сильно влияют на производительность.

  • Когда узел дерева огромен, это приводит к тому, что стек выполнения рекурсивных вызовов становится все глубже и глубже.
  • Выполнение нельзя прервать, страница будет ждать завершения рекурсивного выполнения перед повторным рендерингом.

Подробно объясните Dom-Diff в React

Что такое клетчатка

  • Волокно — исполнительный блок
  • Волокно также является структурой данных

Волокно — исполнительный блок

над浏览器任务调度过程Также есть упоминание на странице синтеза фазы холостого фазыrequestIdleCallback.

На следующем рисунке показан процесс планирования React в сочетании с фазой простоя.

fiber-flow

Этосовместное планирование, требует, чтобы программа и браузер доверяли друг другу. Браузер, как лидер, выделяет временной интервал выполнения (т. е. requestIdleCallback) программе для выбора вызова. Программа должна завершить выполнение в течение этого времени, как было согласовано, и вернуть управление браузеру.

Волокно представляет собой блок выполнения, каждый раз, когда полные блоки выполнения реагируют, будут проверять, сколько времени осталось сейчас, если нет контроля за временем, он будет возвращен в браузер; затем перейдите к появлению следующего кадра.

Волокно также является структурой данных

fiber-structure

Использование связанных списков в ReactVirtual DOMСвязанный, каждый узел представляет волокно

class FiberNode {
  constructor(type, payload) {
    this.type = type // 节点类型
    this.key = payload.key // key
    this.payload = payload // 挂载的数据
    this.return = null // 父Fiber
    this.child = null // 长子Fiber
    this.sibling = null // 相邻兄弟Fiber
  }
}

// Test
const A1 = new FiberNode('div', { key: 'A1' })
const B1 = new FiberNode('div', { key: 'B1' })
const B2 = new FiberNode('div', { key: 'B2' })
const C1 = new FiberNode('div', { key: 'C1' })
const C2 = new FiberNode('div', { key: 'C2' })

A1.child = B1
B1.return = A1
B1.sibling = B2
B1.child = C1
B2.return = A1
C1.return = B1
C1.sibling = C2
C2.return =  B1

Сводка по волокну

  • Мы можем разумно распределять ресурсы ЦП с помощью определенных стратегий планирования, чтобы улучшить скорость отклика пользователя.
  • Благодаря архитектуре Fiber ваш собственный процесс согласования может быть прерван, а право на выполнение ЦП освобождается своевременно, что позволяет браузеру своевременно реагировать на действия пользователя.

Фаза выполнения волокна

Каждый рендер состоит из двух фаз:Reconciliationэтап (координация/рендеринг) иCommit(совершить) этап

  • Фаза координации/рендеринга: ее можно рассматривать как фазу Diff.Эта фаза может быть прервана.На этой фазе будут обнаружены все изменения узлов,такие как добавление узлов,удаление и т.д.Эти изменения называются Эффектами (побочными эффектами) в React.
  • Этап фиксации: выполнение побочных эффектов, рассчитанных на предыдущем этапе, которые необходимо обработать за один раз. Этот этап не может быть прерван и должен выполняться синхронно в одно время.

Этап согласования

Ниже приводится комбинация вышеупомянутых точек знаний.

Тестовый пример на данном этапеfiberRender.html, основной код хранитсяfiberRender.js.

надFiber也是一种数据结构Резюме Дерево волокон построено, затем начинается обход, при первом рендеринге добавляются все типы операций.

согласно сVirtual DOMстроитьFiber Tree

nextUnitOfWork = A1
requestIdleCallback(workLoop, { timeout: 1000 })

Свободное время для прохождения коллекцииA1корневой узел

function workLoop (deadline) {
  // 这一帧渲染还有空闲时间 || 没超时 && 还存在一个执行单元
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork) // 执行当前执行单元 并返回下一个执行单元
  }
  if (!nextUnitOfWork) {
    console.log('render end !')
  } else {
    requestIdleCallback(workLoop, { timeout: 1000 })
  }
}
  • когда удовлетворен这一帧渲染还有空闲时间或没超时 && 还存在一个执行单元для выполнения текущей исполнительной единицы и возврата к следующей исполнительной единице.
  • После того, как вышеперечисленные условия не выполнены, если исполнительный блок все еще есть, рендеринг следующего кадра продолжится.
    • Эта фаза завершается, когда исполнительный модуль не существует.
function performUnitOfWork (fiber) {
  beginWork(fiber) // 开始
  if (fiber.child) {
    return fiber.child
  }
  while (fiber) {
    completeUnitOfWork(fiber) // 结束
    if (fiber.sibling) {
      return fiber.sibling
    }
    fiber = fiber.return
  }
}
function beginWork (fiber) {
  console.log('start: ', fiber.key)
}
function completeUnitOfWork (fiber) {
  console.log('end: ', fiber.key)
}

fiber-traversПроцесс прохождения блока выполнения выглядит следующим образом

  1. Если нет старшего ребенка, он указывает на то, что текущий прохождение узла завершен.completeUnitOfWorkсобрано в
  2. Если смежных братьев и сестер нет, родительский узел возвращается, чтобы указать, что обход родительского узла завершен.completeUnitOfWorkсобрано в
  3. Если родительского узла нет, это означает, что весь обход завершен.over
  4. Если есть старший ребенок, пройдите через него;beginWorkсобрано; возвращено старшему сыну после сбора, возвращено в第2步Переберите
  5. Если есть соседние братья и сестры, пройдите через;beginWorkсобрано; возвращено старшему сыну после сбора, возвращено в第2步Переберите

Последовательность сбора выполняется следующим образом.

аналогичныйПредварительный обход бинарного дерева

function beginWork (fiber) {
  console.log('start: ', fiber.key) // A1 B1 C1 C2 B2
}

Завершенная последовательность сбора выглядит следующим образом

аналогичныйПостпорядковый обход бинарного дерева

function completeUnitOfWork (fiber) {
  console.log('end: ', fiber.key) // C1 C2 B1 B2 A1
}

Стадия фиксации

похожий наGitФункция ветвления, создание копии из старого дерева и выполнение в новой веткеДобавить, удалить, обновитьдействие до совершения.

git-branch

Тестовый пример на данном этапеfiberCommit.html, основной код хранитсяfiberCommit.js.

Сначала постройте корневое волокно,stateNodeУказывает реальный дом текущего узла.

let container = document.getElementById('root')
workInProgressRoot = {
  key: 'ROOT',
  // 节点实例(状态):
  //        对于宿主组件,这里保存宿主组件的实例, 例如DOM节点
  //        对于类组件来说,这里保存类组件的实例
  //        对于函数组件说,这里为空,因为函数组件没有实例
  stateNode: container,
  props: { children: [A1] }
}
nextUnitOfWork = workInProgressRoot // 从RootFiber开始,到RootFiber结束

как на предыдущем этапеbeginWorkСоберите процесс и доработайте его. То есть расщепить все узлы.

function beginWork(currentFiber) { // ++
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = document.createElement(currentFiber.type) // 创建真实DOM
    for (let key in currentFiber.props) { // 循环属性赋赋值给真实DOM
      if (key !== 'children' && key !== 'key')
        currentFiber.stateNode.setAttribute(key, currentFiber.props[key])
    }
  }
  let previousFiber
  currentFiber.props.children.forEach((child, index) => {
    let childFiber = {
      tag: 'HOST',
      type: child.type,
      key: child.key,
      props: child.props,
      return: currentFiber,
      // 当前节点的副作用类型,例如节点更新、删除、移动
      effectTag: 'PLACEMENT',
      // 和节点关系一样,React 同样使用链表来将所有有副作用的Fiber连接起来
      nextEffect: null
    }
    if (index === 0) {
      currentFiber.child = childFiber
    } else {
      previousFiber.sibling = childFiber
    }
    previousFiber = childFiber
  })
}

вeffectTagИдентифицирует тип побочного эффекта текущего узла, первый рендеринг является новым.PLACEMENT,nextEffectИдентифицирует следующий узел с побочными эффектами.

Тогда идеальноcompleteUnitOfWork(Завершенная коллекция).

function completeUnitOfWork(currentFiber) { // ++
  const returnFiber = currentFiber.return
  if (returnFiber) {
    if (!returnFiber.firstEffect) {
      returnFiber.firstEffect = currentFiber.firstEffect
    }
    if (currentFiber.lastEffect) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
      }
      returnFiber.lastEffect = currentFiber.lastEffect
    }

    if (currentFiber.effectTag) {
      if (returnFiber.lastEffect) {
        returnFiber.lastEffect.nextEffect = currentFiber
      } else {
        returnFiber.firstEffect = currentFiber
      }
      returnFiber.lastEffect = currentFiber
    }
  }
}

Цель состоит в том, чтобы сформировать завершенную коллекцию в структуру связанного списка, сcommitRootсцена.

когда все执行、完成После того, как коллекция завершена (то есть все реальные DOM, виртуальные DOM и Fiber объединены, а их побочные эффекты (добавление, удаление и изменение) образуют структуру связанного списка), их необходимо отобразить на странице.

function workLoop (deadline) {
  // ...
  if (!nextUnitOfWork) {
    console.log('render end !')
    commitRoot()
  } else {
    requestIdleCallback(workLoop, { timeout: 1000 })
  }
}

Найдите первый узел волокна с завершенными побочными эффектами, рекурсивноappendChildк родительскому элементу.

function commitRoot() { // ++
  let fiber = workInProgressRoot.firstEffect
  while (fiber) {
    console.log('complete: ', fiber.key) // C1 C2 B1 B2 A1
    commitWork(fiber)
    fiber = fiber.nextEffect
  }
  workInProgressRoot = null
}
function commitWork(currentFiber) {
  currentFiber.return.stateNode.appendChild(currentFiber.stateNode)
}

Ниже приведен порядок сбора вышеуказанных эффектов рендеринга и печати.

fiber-commit-result

React использует волокно

Подготовьте среду

использоватьreact-create-appСоздать проектfiber

// src/index.js
import React from 'react'
let element = (
  <div id="A1">
    <div id="B1">
      <div id="C1"></div>
      <div id="C2"></div>
    </div>
    <div id="B2"></div>
  </div>
)
console.log(element);

npm i && npm startЗатем распечатайте результат следующим образом

react-vdom

Заимствуя вавилонскую компиляцию строительных лесов, пишем прямоJSX语法код.

Реализовать метод createElement

существуетbabelвремя компиляции будетJSXСинтаксис преобразуется в объект, а затем вызов под реагированиемReact.createElementспособ построить виртуальный дом. Можем смоделировать следующим образом:

// core/react.js
const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
function createElement(type, config, ...children) {
  return {
    type, // 元素类型
    props: {
      ...config,
      children: children.map(
        child => typeof child === "object" ?
          child :
          { type: ELEMENT_TEXT, props: { text: child, children: [] } })
    }
  }
}

let React = {
  createElement
}
export default React;

еслиchildrenЕсть ребенокReact.createElementвернутьReact元素, а если это строка, то она будет преобразована в текстовый узел.

Реализовать первый рендер

Подготовьте следующую структуру

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
let style = { border: '3px solid green', margin: '5px' };
let element = (
  <div id="A1" style={style}>
    A1
    <div id="B1" style={style}>
      B1
      <div id="C1" style={style}>C1</div>
      <div id="C2" style={style}>C2</div>
    </div>
    <div id="B2" style={style}>B2</div>
  </div>
)
ReactDOM.render(
  element,
  document.getElementById('root')
);

Желаемый результат рендеринга

react-target-render

На этом этапе вам нужно определить некоторые константы столбца.

// core/constants.js
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT'); // 文本元素
export const TAG_ROOT = Symbol.for('TAG_ROOT'); // 根Fiber
export const TAG_HOST = Symbol.for('TAG_HOST'); // 原生的节点 span div p 函数组件 类组件
export const TAG_TEXT = Symbol.for('TAG_TEXT'); // 文本节点
export const PLACEMENT = Symbol.for('PLACEMENT'); // 插入节点

Затем с помощью вышеуказанногоReconciliation阶段,существуетreact-dom.jsСначала создайте виртуальный дом в дереве волокон.

// core/react-dom.js
import { TAG_ROOT } from './constants';
import { scheduleRoot } from './scheduler';
function render(element, container) {
  let rootFiber = {
    tag: TAG_ROOT, // 这是根Fiber
    stateNode: container, // 此Fiber对应的DOM节点
    props: { children: [element] }, // 子元素就是要渲染的element
  }
  scheduleRoot(rootFiber);
}

export default {
  render
}

затем передатьscheduleRootпланировать

// core/scheduler.js
// ...

Объем кода большой, в основном дляReconciliation阶段а такжеCommit阶段комбинированный код.

Этот адрес хранения кода процесса

из которыхbeginWorkутонченность

function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) { // 如果是根节点
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) { // 如果是原生文本节点
    updateHostText(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) { // 如果是原生DOM节点
    updateHostComponent(currentFiber);
  }
}
function updateHostRoot(currentFiber) { // 如果是根节点
  const newChildren = currentFiber.props.children; // 直接渲染子节点
  reconcileChildren(currentFiber, newChildren);
}
function updateHostText(currentFiber) {
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber); // 先创建真实的DOM节点
  }
}
function updateHostComponent(currentFiber) { // 如果是原生DOM节点
  if (!currentFiber.stateNode) {
    currentFiber.stateNode = createDOM(currentFiber); // 先创建真实的DOM节点
  }
  const newChildren = currentFiber.props.children;
  reconcileChildren(currentFiber, newChildren);
}

Среди них в основном присваивается разным типам узловstateNode

  • Собственный узел DOM/собственный текстовый узел: напрямую создайте настоящий узел DOM и назначьте егоstateNode
  • Он будет расширен ниже
    • Компоненты класса: нужен новый экземпляр компонента для монтажаstateNode
    • функциональные компоненты: нет экземпляра,stateNodeнулевой

reconcileChildrenОн также имеет дело с различными типами узлов.

Сводка по рендерингу

Снова объедините два этапа и правила планирования следующего раздела.

  • Существует два основных этапа рендеринга и планирования из корневого узла.
    • Этап рендеринга: этот этап занимает много времени, мы можем разделить задачу и разделить измерение виртуального DOM. На данном этапе использованиеrequestIdleCallbackВы можете добиться паузы
    • этап сравнения: сравните старый и новый виртуальный DOM и выполните приращение, обновление и создание
  • Результат этапа рендерингаeffect list, собирать добавления, удаления и изменения узлов
  • Фаза рендеринга имеет две задачи
    • Сгенерируйте дерево волокон на основе виртуального DOM
    • собиратьeffectlist
  • На этапе коммита выполняется этап обновления и создания DOM, этот этап не может быть приостановлен и должен быть завершен за один раз.

правила планирования

  • Цепное правило обхода: сначала старший сын, затем брат, затем второй дядя.
  • Правило цепочки завершения: если все сыновья завершены, они будут завершены сами по себе.
  • цепочка эффектов: такая же, как цепочка завершения

обновить элемент

Среди них используется стратегия оптимизации с двойной буферизацией, и следующее будет посвящено введению

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

Манипулируйте страницей, а затем повторно визуализируйте, ожидая, что первое обновление будет变更A1/B1/C1/C2、新增B3, второе обновление变更A1/B1/C1/C2、删除B3.

react-target-reRender

Соответствующий новый код выглядит следующим образом

<!-- public/index.html -->
<div id="root"></div>
<button id="reRender1">reRender1</button>
<button id="reRender2">reRender2</button>
<button id="reRender3">reRender3</button>

Связывание событий для двух кнопок, отрисовка страницы

// src/index.js
let reRender2 = document.getElementById('reRender2');
reRender2.addEventListener('click', () => {
  let element2 = (
    <div id="A1-new" style={style}>
      A1-new
      <div id="B1-new" style={style}>
        B1-new
        <div id="C1-new" style={style}>C1-new</div>
        <div id="C2-new" style={style}>C2-new</div>
      </div>
      <div id="B2" style={style}>B2</div>
      <div id="B3" style={style}>B3</div>
    </div>
  )
  ReactDOM.render(
    element2,
    document.getElementById('root')
  );
});

let reRender3 = document.getElementById('reRender3');
reRender3.addEventListener('click', () => {
  let element3 = (
    <div id="A1-new2" style={style}>
      A1-new2
      <div id="B1-new2" style={style}>
        B1-new2
        <div id="C1-new2" style={style}>C1-new2</div>
        <div id="C2-new2" style={style}>C2-new2</div>
      </div>
      <div id="B2" style={style}>B2</div>
    </div>
  )
  ReactDOM.render(
    element3,
    document.getElementById('root')
  );
});

Стратегия обновления с двойной буферизацией

fiber-update-process-1

fiber-update-process-2

  • Назначайте дерево волокон после каждого рендерингаcurrentRoot
  • Первое обновлениеrooterFiberизalternateнаправление上一次渲染好的currentRoot
  • Обновления после второго будутworkInProgressRootнаправлениеcurrentRoot.alternate, то токworkInProgressRoot.alternateнаправление上一次渲染好的currentRoot
  • ...
  • А затем добиться повторного использования дерева объектов волокна
Код изменения выглядит следующим образом
import { setProps } from './utils';
import {
    ELEMENT_TEXT, TAG_ROOT, TAG_HOST, TAG_TEXT, PLACEMENT, DELETION, UPDATE
} from './constants';
+let currentRoot = null;//当前的根Fiber
let workInProgressRoot = null;//正在渲染中的根Fiber
let nextUnitOfWork = null//下一个工作单元
+let deletions = [];//要删除的fiber节点

export function scheduleRoot(rootFiber) {
  // {tag:TAG_ROOT,stateNode:container,props: { children: [element] }}
+  if (currentRoot && currentRoot.alternate) {//偶数次更新
+    workInProgressRoot = currentRoot.alternate;
+    workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
+    workInProgressRoot.props = rootFiber.props;
+    workInProgressRoot.alternate = currentRoot;
+  } else if (currentRoot) {//奇数次更新
+    rootFiber.alternate = currentRoot;
+    workInProgressRoot = rootFiber;
+  } else {
+    workInProgressRoot = rootFiber;//第一次渲染
+  }
    nextUnitOfWork = workInProgressRoot;
}

function commitRoot() {
+  deletions.forEach(commitWork);
  let currentFiber = workInProgressRoot.firstEffect;
  while (currentFiber) {
    commitWork(currentFiber);
    currentFiber = currentFiber.nextEffect;
  }
+  deletions.length = 0;//先把要删除的节点清空掉
+  currentRoot = workInProgressRoot;
  workInProgressRoot = null;
}
function commitWork(currentFiber) {
  if (!currentFiber) {
    return;
  }
  let returnFiber = currentFiber.return;//先获取父Fiber
  const domReturn = returnFiber.stateNode;//获取父的DOM节点
  if (currentFiber.effectTag === PLACEMENT && currentFiber.stateNode != null) {//如果是新增DOM节点
    let nextFiber = currentFiber;
    domReturn.appendChild(nextFiber.stateNode);
+  } else if (currentFiber.effectTag === DELETION) {//如果是删除则删除并返回
+      domReturn.removeChild(currentFiber.stateNode);
+  } else if (currentFiber.effectTag === UPDATE && currentFiber.stateNode != null) {//如果是更新
+    if (currentFiber.type === ELEMENT_TEXT) {
+      if (currentFiber.alternate.props.text != currentFiber.props.text) {
+        currentFiber.stateNode.textContent = currentFiber.props.text;
+      }
+    } else {
+      updateDOM(currentFiber.stateNode, currentFiber.alternate.props, currentFiber.props);
+    }
+  }
  currentFiber.effectTag = null;
}

function reconcileChildren(currentFiber, newChildren) {
  let newChildIndex = 0;//新虚拟DOM数组中的索引
+  let oldFiber = currentFiber.alternate && currentFiber.alternate.child;//父Fiber中的第一个子Fiber
+  let prevSibling;
+  while (newChildIndex < newChildren.length || oldFiber) {
+    const newChild = newChildren[newChildIndex];
+    let newFiber;
+    const sameType = oldFiber && newChild && newChild.type === oldFiber.type;//新旧都有,并且元素类型一样
+    let tag;
+    if (newChild && newChild.type === ELEMENT_TEXT) {
+      tag = TAG_TEXT;//文本
+    } else if (newChild && typeof newChild.type === 'string') {
+      tag = TAG_HOST;//原生DOM组件
+    }
+    if (sameType) {
+      if (oldFiber.alternate) {
+        newFiber = oldFiber.alternate;
+        newFiber.props = newChild.props;
+        newFiber.alternate = oldFiber;
+        newFiber.effectTag = UPDATE;
+        newFiber.nextEffect = null;
+      } else {
+        newFiber = {
+          tag:oldFiber.tag,//标记Fiber类型,例如是函数组件或者原生组件
+          type: oldFiber.type,//具体的元素类型
+          props: newChild.props,//新的属性对象
+          stateNode: oldFiber.stateNode,//原生组件的话就存放DOM节点,类组件的话是类组件实例,函数组件的话为空,因为没有实例
+          return: currentFiber,//父Fiber
+          alternate: oldFiber,//上一个Fiber 指向旧树中的节点
+          effectTag: UPDATE,//副作用标识
+          nextEffect: null //React 同样使用链表来将所有有副作用的Fiber连接起来
+        }
# +      }
+      } else {
+        if (newChild) {//类型不一样,创建新的Fiber,旧的不复用了
+          newFiber = {
+            tag,//原生DOM组件
+            type: newChild.type,//具体的元素类型
+            props: newChild.props,//新的属性对象
+            stateNode: null,//stateNode肯定是空的
+            return: currentFiber,//父Fiber
+            effectTag: PLACEMENT//副作用标识
+          }
+        }
+        if (oldFiber) {
+          oldFiber.effectTag = DELETION;
+          deletions.push(oldFiber);
+        }
+      }
+      if (oldFiber) {  //比较完一个元素了,老Fiber向后移动1位
+        oldFiber = oldFiber.sibling;
+      }
    if (newFiber) {
      if (newChildIndex === 0) {
        currentFiber.child = newFiber;//第一个子节点挂到父节点的child属性上
      } else {
        prevSibling.sibling = newFiber;
      }
      prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
    }
    prevSibling = newFiber;//然后newFiber变成了上一个哥哥了
    newChildIndex++;
  }
}

реализовать компонент класса

fiber-classCom-process-1построить счетчик

class ClassCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { number: 0 };
  }
  onClick = () => {
    this.setState(state => ({ number: state.number + 1 }));
  }
  render() {
    return (
      <div id="counter">
        <span>{this.state.number}</span>
        <button onClick={this.onClick}>加1</button>
      </div >
    )
  }
}
ReactDOM.render(
  <ClassCounter />,
  document.getElementById('root')
);
import { ELEMENT_TEXT } from './constants';
+import { Update, UpdateQueue } from './updateQueue';
+import { scheduleRoot } from './scheduler';
// ...
+class Component {
+  constructor(props) {
+    this.props = props;
+    this.updateQueue = new UpdateQueue();
+  }
+  setState(payload) {
+    this.internalFiber.updateQueue.enqueueUpdate(new Update(payload));
+    scheduleRoot();
+  }
+}
+Component.prototype.isReactComponent = true;
let React = {
    createElement,
+  Component
}
export default React;

Этот процесс находится вмакет setStateПроцесс был объяснен

export class Update {
  constructor(payload) {
    this.payload = payload;
  }
}
// 数据结构是一个单链表
export class UpdateQueue {
  constructor() {
    this.firstUpdate = null;
    this.lastUpdate = null;
  }
  enqueueUpdate(update) {
    if (this.lastUpdate === null) {
      this.firstUpdate = this.lastUpdate = update;
    } else {
      this.lastUpdate.nextUpdate = update;
      this.lastUpdate = update;
    }
  }
  forceUpdate(state) {
    let currentUpdate = this.firstUpdate;
    while (currentUpdate) {
      let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
      state = { ...state, ...nextState };
      currentUpdate = currentUpdate.nextUpdate;
    }
    this.firstUpdate = this.lastUpdate = null;
    return state;
  }
}

нуждаться вsrc/scheduler.jsВнесите следующие изменения в файл

function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) {//如果是根节点
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
    updateHostText(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
    updateHostComponent(currentFiber);
+  } else if (currentFiber.tag === TAG_CLASS) {//如果是类组件
+    updateClassComponent(currentFiber)
+  }
}
+function updateClassComponent(currentFiber) {
+  if (currentFiber.stateNode === null) {
+    currentFiber.stateNode = new currentFiber.type(currentFiber.props);
+    currentFiber.stateNode.internalFiber = currentFiber;
+    currentFiber.updateQueue = new UpdateQueue();
+  }
+  currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state);
+  const newChildren = [currentFiber.stateNode.render()];
+  reconcileChildren(currentFiber, newChildren);
+}

Если это компонент класса, новый класс будет кэшировать экземпляр вcurrentFiber.stateNode, а потом实例的render()方法执行结果рекурсивное планированиеreconcileChildren

Реализовать функциональные компоненты

Подобные компоненты одинаковы, добавьте новую копию в каждое соответствующее местоelse..ifТолько что

function FunctionCounter() {
  return (
    <h1>
      Count:0
    </h1>
  )
}
ReactDOM.render(
  <FunctionCounter />,
  document.getElementById('root')
);
function beginWork(currentFiber) {
  if (currentFiber.tag === TAG_ROOT) {//如果是根节点
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_TEXT) {//如果是原生文本节点
    updateHostText(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) {//如果是原生DOM节点
    updateHostComponent(currentFiber);
  } else if (currentFiber.tag === TAG_CLASS) {//如果是类组件
    updateClassComponent(currentFiber)
+  } else if (currentFiber.tag === TAG_FUNCTION) {//如果是函数组件
+    updateFunctionComponent(currentFiber);
+  }
}
+function updateFunctionComponent(currentFiber) {
+  const newChildren = [currentFiber.type(currentFiber.props)];
+  reconcileChildren(currentFiber, newChildren);
+}

Отличие компонентов класса в том, что функциональный компонент не имеет экземпляра, поэтому рекурсивное возвращаемое значение выполняется функцией напрямую.

Реализовать крючки

Используйте следующим образом

// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
// import React from '../../../packages/fiber/core/react';
// import ReactDOM from '../../../packages/fiber/core/react-dom';

function reducer(state, action) {
  switch (action.type) {
    case 'ADD':
      return { count: state.count + 1 };
    default:
      return state;
  }
}
function FunctionCounter() {
  const [numberState, setNumberState] = React.useState({ number: 0 });
  const [countState, dispatch] = React.useReducer(reducer, { count: 0 });
  return (
    <div>
      <h1 onClick={() => setNumberState(state => ({ number: state.number + 1 }))}>
        Count: {numberState.number}
      </h1 >
      <hr />
      <h1 onClick={() => dispatch({ type: 'ADD' })}>
        Count: {countState.count}
      </h1 >
    </div>
  )
}
ReactDOM.render(
  <FunctionCounter />,
  document.getElementById('root')
);

Требуется реагировать, чтобы предоставитьuseState/useReducerдва крючка

// core/react.js
+import { scheduleRoot,useState,useReducer} from './scheduler';
let React = {
  createElement,
  Component,
+  useState,
+  useReducer
}

Процесс внедрения выглядит следующим образом

// core/scheduler.js
+import { UpdateQueue, Update } from './updateQueue';
+let workInProgressFiber = null; //正在工作中的fiber
+let hookIndex = 0;              //hook索引
function updateFunctionComponent(currentFiber) {
+  workInProgressFiber = currentFiber;
+  hookIndex = 0;
+  workInProgressFiber.hooks = [];
  const newChildren = [currentFiber.type(currentFiber.props)];
  reconcileChildren(currentFiber, newChildren);
}
+export function useReducer(reducer, initialValue) {
+  let oldHook =
+    workInProgressFiber.alternate &&
+    workInProgressFiber.alternate.hooks &&
+    workInProgressFiber.alternate.hooks[hookIndex];
+  let newHook = oldHook;
+  if (oldHook) {
+    oldHook.state = oldHook.updateQueue.forceUpdate(oldHook.state);
+  } else {
+    newHook = {
+      state: initialValue,
+      updateQueue: new UpdateQueue()
+    };
+  }
+  const dispatch = action => {
+    newHook.updateQueue.enqueueUpdate(
+      new Update(reducer ? reducer(newHook.state, action) : action)
+    );
+    scheduleRoot();
+  }
+  workInProgressFiber.hooks[hookIndex++] = newHook;
+  return [newHook.state, dispatch];
+}
+export function useState(initState) {
+  return useReducer(null, initState)
+}

Суммировать

Прочитав приведенную выше очень простую реализацию, давайте рассмотрим первые несколько вопросов:

  • Каковы болевые точки в React15? Что такое клетчатка? Почему React16 нужно представить Fiber?
    • Этапы рендеринга и сравнения выполняются за один раз, и страница застрянет, когда дерево узлов огромно.
    • Волокно не загадочно, оно просто превращает Virtual-DOM в структуру связанного списка.
    • Структура таблицы ссылок Metallion RequestidLeCallback обеспечивает механизм прерываемого планирования
  • Как реализовать виртуальный DOM под React16?
    • Так же, как Rect15.
  • Как реализовать структуру данных и алгоритм обхода Fiber?
  • Как реализовать прерываемое и возобновляемое планирование задач в архитектуре Fiber?
    • Как указать количество для обновления? Как выполнить пакетное обновление?
    • С помощью requestIdleCallback браузер может реализовать заданное количество обновлений в простое, отдаваемом после одного кадра рендеринга.Пакетные обновления могут напрямую пропускать этот API, и следовать предыдущему методу.
  • Как реализовать рендеринг компонентов и отправку коллекции побочных эффектов в архитектуре Fiber?
    • Выполняемый порядок сбора аналогичен предварительному обходу двоичного дерева.
    • Завершенный порядок сбора аналогичен обходу двоичного дерева в обратном порядке.
  • Как реализовать стратегию оптимизации согласования и двойной буферизации в Fiber?
    • Добавьте альтернативное поле в структуру Fiber, чтобы идентифицировать последнее отображаемое дерево Fiber, которое можно повторно использовать при следующем отображении.
  • Как реализовать такие хуки, как useReducer и useState?
  • Как реализовать приоритет задачи expireTime, планирование задач и обработку тайм-аута?
  • Как реализовать оптимизированную обработку ключей для согласования domdiff?
  • Как реализовать синтетическое событие SyntheticEvent?
  • Как реализовать реф useEffect?

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

использованная литература