Цель состоит в том, чтобы познакомиться с волокном и реализовать основные функции реакции.Пожалуйста, прочитайте эту статью со следующими вопросами.
- Каковы болевые точки в React15? Что такое клетчатка? Почему React16 нужно представить Fiber?
- Как реализовать виртуальный DOM под React16?
- Как реализовать структуру данных и алгоритм обхода Fiber?
- Как реализовать прерываемое и возобновляемое планирование задач в архитектуре Fiber?
- Как указать количество для обновления? Как выполнить пакетное обновление?
- Как реализовать рендеринг компонентов и отправку коллекции побочных эффектов в архитектуре Fiber?
- Как реализовать стратегию оптимизации согласования и двойной буферизации в Fiber?
- Как реализовать такие хуки, как useReducer и useState?
- Как реализовать приоритет задачи expireTime, планирование задач и обработку тайм-аута?
- Как реализовать оптимизированную обработку ключей для согласования domdiff?
- Как реализовать синтетическое событие SyntheticEvent?
- Как реализовать реф useEffect?
Эта статья первая@careteen/react, укажите источник для перепечатки. На складе хранятся все коды реализации и примеры, а желающие могут сделать форк для отладки.
содержание
- Стратегия планирования React15
- Стратегия планирования задач браузера и процесс рендеринга
- Преимущества связанных списков
- Волоконная архитектура
- React использует волокно
- использованная литература
Стратегия планирования React15
JavaScript похож на улицу с односторонним движением.
JavaScript работает в одном потоке. В среде браузера он должен отвечать за синтаксический анализ и выполнение страницы JavaScript, рендеринг, обработку событий, загрузку и обработку статических ресурсов. И только одна задача может быть выполнена одна задача, Если одна из задач занимает много времени, последующие задачи не могут быть выполнены, и браузер будет казаться зависшим.
Рендеринг React15 и diff будут рекурсивно сравниватьсяVirtualDOM树
,Найдите узел, который был изменен, а затем обновите их синхронно.Весь процесс в одном идет. Так что, если количество узлов страниц очень большой,React
Мы ожидаем, что в ответ на эти болевые точкиЕсть добавления и удаления для идентификации узла, а затем синхронизация для обновления ихПроцесс разбит на две отдельные части, или есть способ сделать весь процесспрерываемое и возобновляемое выполнение, аналогично однопроцессорному планированию многозадачных операционных систем.
Чтобы реализовать параллелизм процессов, операционная система будет распределять права выполнения ЦП на несколько процессов в соответствии с определенной стратегией планирования, и несколько процессов имеют возможность выполняться, позволяя им выполняться поочередно, создавая иллюзию работает одновременно. Поскольку процессор такой быстрый, люди его вообще не чувствуют. Фактически в одноядерной физической среде одновременно работает только одна программа.
Стратегия планирования задач браузера и процесс рендеринга
Для игры требуется плавная частота обновления, которая составляет не менее 60 Гц. В противном случае игровой опыт оставляет желать лучшего.
Так что же содержит кадр?
Средний кадр составляет 16,66 мс, который в основном делится на следующие части:
- выполнение скрипта
- расчет стиля
- макет
- перерисовать
- синтез
Вычисление скрипта будет выполнено до того, как вычисление стиля будет использовано дляrequestAnimationFrame
Перезвоните
Если вы еще не знаетеrequestAnimationFrame, перейдите на mdn, чтобы увидеть пример реализованного индикатора выполнения.
После синтеза также空闲阶段
, то есть если синтез и все предыдущие этапы трудоемки16.66ms
, в остальное время браузер дает намrequestIdleCallback
Звоните, в полной мере использовать их.
requestIdleCallbackВ настоящее время поддерживает только хром, нужноpolyfill
Общий процесс выглядит следующим образом:
requestIdleCallback позволяет разработчикам выполнять фоновую и низкоприоритетную работу в основном цикле событий, не влияя на критичные к задержке события, такие как анимация и ответы ввода.
Преимущества связанных списков
Поскольку размер массива фиксирован, вставка или удаление элементов из начала или середины массива требует больших затрат. Преимущество связанных списков по сравнению с традиционными массивами заключается в том, что добавление или удаление элементов не требует перемещения других элементов.Когда вам нужно добавить и удалить много элементов, лучшим выбором будет список, а не массив.Связанные списки играют большую роль в архитектуре React Fiber и реализации хуков.
макет 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.
const root = {
key: 'A1',
children: [
{
key: 'B1',
children: [
{
key: 'C1',
children: []
},
{
key: 'C2',
children: []
}
]
},
{
key: 'B2',
children: []
}
]
}
Он проходится с использованием алгоритма поиска в глубину.
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
То же самое относится и к рекурсивному сравнению обхода, и есть две проблемы, которые очень сильно влияют на производительность.
- Когда узел дерева огромен, это приводит к тому, что стек выполнения рекурсивных вызовов становится все глубже и глубже.
- Выполнение нельзя прервать, страница будет ждать завершения рекурсивного выполнения перед повторным рендерингом.
Что такое клетчатка
- Волокно — исполнительный блок
- Волокно также является структурой данных
Волокно — исполнительный блок
над浏览器任务调度过程
Также есть упоминание на странице синтеза фазы холостого фазыrequestIdleCallback
.
На следующем рисунке показан процесс планирования React в сочетании с фазой простоя.
Этосовместное планирование, требует, чтобы программа и браузер доверяли друг другу. Браузер, как лидер, выделяет временной интервал выполнения (т. е. requestIdleCallback) программе для выбора вызова. Программа должна завершить выполнение в течение этого времени, как было согласовано, и вернуть управление браузеру.
Волокно представляет собой блок выполнения, каждый раз, когда полные блоки выполнения реагируют, будут проверять, сколько времени осталось сейчас, если нет контроля за временем, он будет возвращен в браузер; затем перейдите к появлению следующего кадра.
Волокно также является структурой данных
Использование связанных списков в 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)
}
Процесс прохождения блока выполнения выглядит следующим образом
- Если нет старшего ребенка, он указывает на то, что текущий прохождение узла завершен.
completeUnitOfWork
собрано в - Если смежных братьев и сестер нет, родительский узел возвращается, чтобы указать, что обход родительского узла завершен.
completeUnitOfWork
собрано в - Если родительского узла нет, это означает, что весь обход завершен.
over
- Если есть старший ребенок, пройдите через него;
beginWork
собрано; возвращено старшему сыну после сбора, возвращено в第2步
Переберите - Если есть соседние братья и сестры, пройдите через;
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
Функция ветвления, создание копии из старого дерева и выполнение в новой веткеДобавить, удалить, обновитьдействие до совершения.
Тестовый пример на данном этапе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)
}
Ниже приведен порядок сбора вышеуказанных эффектов рендеринга и печати.
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
Затем распечатайте результат следующим образом
Заимствуя вавилонскую компиляцию строительных лесов, пишем прямо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')
);
Желаемый результат рендеринга
На этом этапе вам нужно определить некоторые константы столбца.
// 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 и выполните приращение, обновление и создание
- Этап рендеринга: этот этап занимает много времени, мы можем разделить задачу и разделить измерение виртуального DOM. На данном этапе использование
- Результат этапа рендеринга
effect list
, собирать добавления, удаления и изменения узлов -
Фаза рендеринга имеет две задачи
- Сгенерируйте дерево волокон на основе виртуального DOM
- собирать
effectlist
- На этапе коммита выполняется этап обновления и создания DOM, этот этап не может быть приостановлен и должен быть завершен за один раз.
правила планирования
- Цепное правило обхода: сначала старший сын, затем брат, затем второй дядя.
- Правило цепочки завершения: если все сыновья завершены, они будут завершены сами по себе.
- цепочка эффектов: такая же, как цепочка завершения
обновить элемент
Среди них используется стратегия оптимизации с двойной буферизацией, и следующее будет посвящено введению
Подобно широко используемым механизмам рисования в области графики.Технология двойного буфера. Сначала нарисуйте изображение в буфере, а затем одновременно передайте его на экран для отображения, что может предотвратить дрожание экрана и оптимизировать производительность рендеринга.
Манипулируйте страницей, а затем повторно визуализируйте, ожидая, что первое обновление будет变更A1/B1/C1/C2、新增B3
, второе обновление变更A1/B1/C1/C2、删除B3
.
Соответствующий новый код выглядит следующим образом
<!-- 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')
);
});
Стратегия обновления с двойной буферизацией
- Назначайте дерево волокон после каждого рендеринга
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++;
}
}
реализовать компонент класса
построить счетчик
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?
Но есть еще несколько вопросов без ответов, и в следующей статье мы продолжим их изучение...