Автор: Лэн Хуэй
Волоконно-конструкторское мышление
Fiber — это реконструкция основного алгоритма React. Команда Facebook потратила более двух лет на реконструкцию основного алгоритма React. Архитектура Fiber была представлена в версиях выше React16, и идеи дизайна очень достойны изучения.
Зачем вам нужно волокно
Мы знаем, что в браузере страница отрисовывается кадр за кадром, а частота кадров рендеринга соответствует частоте обновления устройства. В нормальных условиях частота обновления экрана устройства составляет 60 раз в секунду, когда количество кадров в секунду (FPS) превышает 60, рендеринг страницы происходит плавно, когда FPS меньше 60, будет определенная степень явления заикания. Давайте посмотрим, что именно делается в полной рамке:
- Сначала нам нужно обработать событие ввода, позволяя пользователям получить отзыв от самого раннего
- Следующим шагом является обработка таймера, вам нужно проверить, достиг ли таймер времени, и выполнить соответствующий обратный вызов
- Далее обработайте Begin Frame (стартовый кадр), то есть события каждого кадра, включая window.resize, прокрутку, изменение медиазапроса и т.д.
- Далее выполняем запрос кадра анимации requestAnimationFrame (rAF), то есть перед каждой отрисовкой будет выполняться колбэк rAF
- Затем следует операция «Макет», в том числе расчет макета и обновление макета, то есть, каков стиль этого элемента и как он должен отображаться на странице.
- Затем выполните операцию Paint, чтобы получить такую информацию, как размер и положение каждого узла в дереве, и браузер заполнит содержимое для каждого элемента.
- К этому моменту вышеуказанные шесть этапов завершены, а затем на этапе простоя (Idle Peroid) вы можете выполнять задачи, зарегистрированные в requestIdleCallback в это время (этот requestIdleCallback будет подробно рассмотрен позже, что является основой React Реализация оптоволокна)
Механизм js и механизм рендеринга страницы находятся в одном потоке рендеринга, и они являются взаимоисключающими. Если выполнение задачи особенно долгое на определенном этапе, например, на этапе таймера илиBegin Frame
Время выполнения этапа очень велико, и время, очевидно, превысило 16 мс, тогда рендеринг страницы будет заблокирован, что приведет к зависанию.
До введения архитектуры Fiber в react16, реакция рекурсивно сравнивала виртуальное дерево DOM, находила узлы, которые необходимо изменить, и затем синхронно обновляла их.Этот процесс называется реакцией.reconcilation
(координация). существуетreconcilation
В течение этого периода реакция всегда будет занимать ресурсы браузера, что приведет к тому, что события, инициированные пользователем, останутся без ответа. Принцип реализации следующий:
Здесь 7 узлов, B1 и B2 — дочерние узлы A1, C1 и C2 — дочерние узлы B1, а C3 и C4 — дочерние узлы B2. Традиционный подход заключается в использовании обхода в глубину для обхода узлов Конкретный код выглядит следующим образом:
const root = {
key: 'A1',
children: [{
key: 'B1',
children: [{
key: 'C1',
children: []
}, {
key: 'C2',
children: []
}]
}, {
key: 'B2',
children: [{
key: 'C3',
children: []
}, {
key: 'C4',
children: []
}]
}]
}
const walk = dom => {
console.log(dom.key)
dom.children.forEach(child => walk(child))
}
walk(root)
Распечатать:
A1
B1
C1
C2
B2
C3
C4
Этот обход является рекурсивным вызовом, стек выполнения будет становиться все глубже и глубже, и его нельзя прервать, и его нельзя возобновить после прерывания. Если рекурсия очень глубокая, она будет очень запаздывающей. Если рекурсия занимает 100 мс, браузер не может ответить в течение 100 мс, и чем больше время выполнения кода, тем более очевидным будет зависание. У традиционного метода есть проблема, заключающаяся в том, что его нельзя прервать, а стек выполнения слишком велик.
Поэтому, чтобы решить вышеуказанные болевые точки, React надеется полностью решить проблему долговременной занятости основного потока, поэтому он представил Fiber, чтобы изменить этот неконтролируемый статус-кво, и разделить процесс рендеринга/обновления на небольшие задачи. Разумный механизм планирования используется для контроля времени и указания времени выполнения задачи, тем самым снижая вероятность зависания страницы и улучшая взаимодействие со страницей. Через архитектуру Fiber пустьreconcilation
Процесс становится прерываемым. Своевременный отказ от права на выполнение ЦП позволяет браузеру своевременно реагировать на действия пользователя.
Fiber используется в React16, но у Vue нет Fiber, почему? Причина в том, что идеи оптимизации двух различны:
- Vue — это обновление на уровне компонентов, основанное на шаблоне и наблюдателе, которое делит каждую задачу обновления на достаточно мелкие и не требует использования архитектуры Fiber, а также делит задачи на более мелкие
- React обновляется из корневого узла независимо от того, где вызывается setState.Задача обновления по-прежнему очень большая.Необходимо использовать Fiber, чтобы разделить большую задачу на несколько маленьких задач, которые можно прерывать и возобновлять, не блокируя основной процесс для выполнить миссию с высоким приоритетом
Теперь давайте погрузимся в мир Fiber и посмотрим, как он реализован.
Что такое клетчатка
Волокно можно понимать как исполнительный блок или структуру данных.
исполнительный блок
Файбер можно понимать как исполнительную единицу, каждый раз при выполнении исполнительной единицы реагировать будет проверять, сколько времени осталось, и если времени нет, то отдавать управление. Основной процесс взаимодействия между React Fiber и браузером выглядит следующим образом:
Во-первых, React запрашивает планирование у браузера. Если у браузера еще есть свободное время во фрейме, он определяет, есть ли задача для выполнения. Если ее нет, он напрямую передает управление браузеру. существует, соответствующая задача будет выполнена.После завершения выполнения будет оцениваться, есть ли еще время.Если есть время и задача, подлежащая выполнению, продолжит выполнение следующей задачи, в противном случае управление будет передано в браузер. Здесь будет небольшой поворот, который можно понять в сочетании с приведенной выше диаграммой.
Волокно можно понимать как деление на более мелкие исполнительные единицы.Он делит большую задачу на множество маленьких задач.Выполнение маленькой задачи должно быть завершено за один раз, и не может быть паузы, а небольшая задача После выполнения, управление может быть передано браузеру для ответа пользователю, так что нет необходимости ждать завершения большой задачи, прежде чем отвечать пользователю, как раньше.
структура данных
Fiber также можно понимать как структуру данных, а React Fiber реализован с использованием связанных списков. Каждый виртуальный DOM может быть представлен как волокно, как показано на рисунке ниже, каждый узел является волокном. Волокно включает в себя такие атрибуты, как дочерний элемент (первый дочерний узел), одноуровневый узел (родственный узел) и возврат (родительский узел).Реализация механизма React Fiber зависит от следующих структур данных. Далее мы поговорим о том, как реализуется Fiber на основе этой структуры связанного списка.
PS: Здесь необходимо объяснить, что волокно является основным алгоритмом реконструкции реагирования, а волокно относится к каждому узлу в структуре данных. Как показано на рисунке ниже, A1 и B1 являются оба волокна.
requestAnimationFrame
Используется в волокнеrequestAnimationFrame, который представляет собой API, предоставляемый браузером для рисования анимации. Требуется, чтобы браузер вызывал указанную функцию обратного вызова, чтобы обновить анимацию перед следующей перерисовкой (т.е. следующим кадром).
Например, я хочу, чтобы браузер увеличивал ширину элемента div страницы на 1 пиксель в каждом кадре, пока ширина не достигнет 100 пикселей и не остановится, тогда я могу использоватьrequestAnimationFrame
для достижения этой функции.
<body>
<div id="div" class="progress-bar "></div>
<button id="start">开始动画</button>
</body>
<script>
let btn = document.getElementById('start')
let div = document.getElementById('div')
let start = 0
let allInterval = []
const progress = () => {
div.style.width = div.offsetWidth + 1 + 'px'
div.innerHTML = (div.offsetWidth) + '%'
if (div.offsetWidth < 100) {
let current = Date.now()
allInterval.push(current - start)
start = current
requestAnimationFrame(progress)
} else {
console.log(allInterval) // 打印requestAnimationFrame的全部时间间隔
}
}
btn.addEventListener('click', () => {
div.style.width = 0
let currrent = Date.now()
start = currrent
requestAnimationFrame(progress)
console.log(allInterval)
})
</script>
Браузер будет увеличивать ширину div на 1 пиксель в каждом кадре, пока не достигнет 100 пикселей. Временной интервал для печати каждого кадра составляет примерно 16 мс.
requestIdleCallback
requestIdleCallbackЭто также базовый API, реализованный React Fiber. Мы хотим иметь возможность быстро реагировать на пользователя, чтобы пользователь чувствовал себя достаточно быстро, чтобы не блокировать взаимодействие пользователя,requestIdleCallback
Позволяет разработчикам выполнять фоновую и низкоприоритетную работу в основном цикле событий, не влияя на критичные к задержке события, такие как анимация и ответы ввода. После завершения обычной задачи кадра она не превышает 16 мс, что указывает на избыточное время простоя, и она будет выполняться в это время.requestIdleCallback
задачи, зарегистрированные в.
Конкретный процесс выполнения выглядит следующим образом: разработчик принимаетrequestIdleCallback
Метод регистрирует соответствующую задачу и сообщает браузеру, что у моей задачи низкий приоритет, и если в каждом кадре есть время простоя, зарегистрированная задача может быть выполнена. Кроме того, разработчики могут пройти вtimeout
Параметр определяет время ожидания. Если время ожидания истекло, браузер должен выполнить его немедленно. Метод использования следующий:window.requestIdleCallback(callback, { timeout: 1000 })
. После того, как браузер выполнит этот метод, если времени не осталось или нет следующей исполняемой задачи, React должен вернуть управление и использовать тот жеrequestIdleCallback
Идите и подайте заявку на следующий временной интервал. Конкретный процесс выглядит следующим образом:
window.requestIdleCallback(callback)
изcallback
получит дедлайн параметра по умолчанию, который содержит следующие два свойства:
- timeRamining возвращает, сколько времени осталось в текущем кадре для использования пользователем
- didTimeout возвращает обратный вызов, если время ожидания задачи истекло
requestIdleCallback
Метод очень важен. Давайте поговорим о двух примерах, чтобы понять этот метод. В каждом примере нужно выполнить несколько задач, но время выполнения задач разное. Давайте посмотрим, как браузер распределяет время на выполнение этих задач:
однокадровое исполнение
Непосредственно выполнить задачу1, задачу2, задачу3, и время каждой задачи составляет менее 16 мс:
let taskQueue = [
() => {
console.log('task1 start')
console.log('task1 end')
},
() => {
console.log('task2 start')
console.log('task2 end')
},
() => {
console.log('task3 start')
console.log('task3 end')
}
]
const performUnitWork = () => {
// 取出第一个队列中的第一个任务并执行
taskQueue.shift()()
}
const workloop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
// 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
// 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
// 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop, { timeout: 1000 })
}
}
requestIdleCallback(workloop, { timeout: 1000 })
Вышеприведенное определяет очередь задачtaskQueue
, и определяетworkloop
функция, которая принимает window.requestIdleCallback(workloop, { timeout: 1000 })
выполнитьtaskQueue
задачи в. выполнять только каждое заданиеconsole.log
Времени очень мало.Браузер считает, что в этом фрейме осталось 15,52мс, что достаточно для выполнения этих трех задач за один раз.Поэтому в простое этого фрейма,taskQueue
Выполняются все три задачи, определенные в . Результат печати следующий:
многокадровое исполнение
Добавьте время сна к задаче1, задаче2 и задаче3, и время их выполнения превышает 16 мс:
const sleep = delay => {
for (let start = Date.now(); Date.now() - start <= delay;) {}
}
let taskQueue = [
() => {
console.log('task1 start')
sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
console.log('task1 end')
},
() => {
console.log('task2 start')
sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
console.log('task2 end')
},
() => {
console.log('task3 start')
sleep(20) // 已经超过一帧的时间(16.6ms),需要把控制权交给浏览器
console.log('task3 end')
}
]
На основе вышеприведенного примера были сделаны некоторые модификации, так чтоtaskQueue
Время выполнения каждой задачи составляет более 16,6 мс.Глядя на результаты печати, мы знаем, что время простоя первого кадра браузера составляет 14 мс, и может быть выполнено только одно задание.Аналогично время во втором и третьего кадра достаточно только выполнить задание. Все эти три задания выполняются отдельно в трех кадрах. Результат печати следующий:
Время кадра браузера не составляет строго 16 мс, его можно динамически контролировать (например, оставшееся время третьего кадра составляет 49,95 мс). Если время подзадачи превышает оставшееся время кадра, оно будет зависать здесь до тех пор, пока подзадача не будет выполнена. Если код имеет бесконечный цикл, браузер зависнет. Если оставшееся время этого кадра больше 0 (имеется время простоя) или истекло время ожидания (время ожидания определено как 1000, которое должно быть принудительно выполнено), и в это время есть задача, задача будет выполняется напрямую. Если времени не осталось, следует отказаться от контроля над выполнением миссии и вернуть выполнение браузеру. Если общее время выполнения нескольких задач меньше времени простоя, возможно выполнение нескольких задач в одном кадре.
Структура списка, связанного с волокном
Структура волокна реализована с использованием связанного списка,Fiber tree
На самом деле это древовидная структура односвязного списка, см. подробностиИсходный код ReactFiber.js, здесь мы видим, что такое структура связанного списка Fiber.После понимания структуры связанного списка мы можем быстрее понять процесс обхода последующего Fiber.
Каждая из вышеперечисленных единиц содержитpayload
(данные) иnextUpdate
(указатель на следующий блок), структура определения выглядит следующим образом:
class Update {
constructor(payload, nextUpdate) {
this.payload = payload // payload 数据
this.nextUpdate = nextUpdate // 指向下一个节点的指针
}
}
Затем определите очередь, объедините каждую единицу и определите два указателя: указатель заголовкаfirstUpdate
и хвостовой указательlastUpdate
, роль состоит в том, чтобы указать на первую единицу и последнюю единицу и добавитьbaseState
Свойства хранят состояние в React. Следующим образом:
class UpdateQueue {
constructor() {
this.baseState = null // state
this.firstUpdate = null // 第一个更新
this.lastUpdate = null // 最后一个更新
}
}
Затем определите два метода: вставить узел узла (enqueueUpdate), обновить очередь (forceUpdate). При вставке узла узла нужно учитывать, есть ли узел уже, если нет, то напрямуюfirstUpdate
,lastUpdate
Просто укажите на этот узел. Очередь обновлений должна проходить по этому связанному списку в соответствии сpayload
обновить содержимое вstate
ценность .
class UpdateQueue {
//.....
enqueueUpdate(update) {
// 当前链表是空链表
if (!this.firstUpdate) {
this.firstUpdate = this.lastUpdate = update
} else {
// 当前链表不为空
this.lastUpdate.nextUpdate = update
this.lastUpdate = update
}
}
// 获取state,然后遍历这个链表,进行更新
forceUpdate() {
let currentState = this.baseState || {}
let currentUpdate = this.firstUpdate
while (currentUpdate) {
// 判断是函数还是对象,是函数则需要执行,是对象则直接返回
let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
currentState = { ...currentState, ...nextState }
currentUpdate = currentUpdate.nextUpdate
}
// 更新完成后清空链表
this.firstUpdate = this.lastUpdate = null
this.baseState = currentState
return currentState
}
}
Наконец, напишите демонстрацию, создайте экземпляр очереди, добавьте в нее множество узлов, а затем обновите очередь:
let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState);
Результат печати следующий:
{ name:'www',age:12 }
Конструкция оптоволоконного узла
Расщепленной единицей волокна является волокно (fiber tree
узел в DOM), который фактически разделен в соответствии с виртуальным узлом DOM, нам нужно сгенерировать дерево Fiber в соответствии с виртуальным DOM. В дальнейшем каждый узел будем называть волокном. Структура волоконного узла выглядит следующим образом, подробности см. в исходном коде.ReactInternalTypes.js.
{
type: any, // 对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, // 唯一标识符
stateNode: any, // 保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, // 大儿子
sibling: Fiber | null, // 下一个兄弟
return: Fiber | null, // 父节点
tag: WorkTag, // 定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, // 指向下一个节点的指针
updateQueue: mixed, // 用于状态更新,回调函数,DOM更新的队列
memoizedState: any, // 用于创建输出的fiber状态
pendingProps: any, // 已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, // 在前一次渲染期间用于创建输出的props
// ……
}
Волоконный узел включает следующие свойства:
(1) тип и ключ
- Тип волокна и ключ работают так же, как элемент React. Тип волокна описывает соответствующий компонент, а для составных компонентов тип — это сама функция или компонент класса. Для нативных тегов (div, span и т. д.) type — это строка. В зависимости от типа ключ используется во время согласования, чтобы определить, можно ли повторно использовать волокно.
(2) узел состояния
- stateNode содержит ссылку на экземпляр класса компонента, узел DOM или другой тип элемента React, связанный с узлом волокна. В общем, это свойство можно рассматривать как сохранение локального состояния, связанного с волокном.
(3) ребенок, брат, сестра и возвращение
- Свойство child указывает на первого потомка этого узла (самого старшего сына).
- Атрибут sibling указывает на следующий одноуровневый узел данного узла (старший сын указывает на второго сына, а второй сын указывает на третьего сына).
- Атрибут return указывает на родительский узел данного узла, то есть на того, кому следует отправлять его результаты после обработки текущего узла. Если волокно имеет несколько дочерних волокон, возвратное волокно каждого дочернего волокна является родительским.
Все узлы волокна имеют следующие атрибуты: дочерний, одноуровневый и возврат для формирования связанного списка узлов волокна (позже мы назовем его связным списком). Как показано ниже:
Другие свойстваmemoizedState
(состояние волокна, создавшего вывод),pendingProps
(реквизит меняется),memoizedProps
(реквизит последнего вывода создания рендера),pendingWorkPriority
(Определить приоритет работы с волокнами) и так далее, я не буду здесь подробно описывать.
Принцип исполнения волокна
Процесс рендеринга и планирования из корневого узла можно разделить на два этапа: этап рендеринга и этап фиксации.
- фаза рендеринга: эту фазу можно прервать, на ней будут обнаружены все изменения узла.
- фаза фиксации: эта фаза непрерывна и будет выполнять все изменения
этап визуализации
На этом этапе будут обнаружены изменения всех узлов, такие как добавление узла, удаление, изменение атрибута и т. д. Эти изменения в совокупности называются побочными эффектами (эффектом), на этом этапе будет построенаFiber tree
, задача разделена с узлом виртуального дома в качестве измерения, то есть узел виртуального дома соответствует задаче, и конечный результатeffect list
, из которого вы можете узнать, какие узлы обновлены, какие узлы добавлены, а какие удалены.
Обход процесса
React Fiber
Первый — преобразовать виртуальное дерево DOM вFiber tree
, так что каждый узел имеетchild
,sibling
,return
собственность, траверсFiber tree
Используется метод обхода после заказа:
- Ход из вершины
- Если есть старший сын, то сначала обходим старшего сына, если нет старшего сына, то обход завершен
- Старший сын: А. Если есть младший брат, вернитесь к младшему брату и перейдите к 2 б) Если нет младшего брата, вернуться к родительскому узлу и отметить завершение обхода родительского узла, перейти к 2 г. Если нет родительского узла, отметить конец обхода
Определите структуру дерева:
const A1 = { type: 'div', key: 'A1' }
const B1 = { type: 'div', key: 'B1', return: A1 }
const B2 = { type: 'div', key: 'B2', return: A1 }
const C1 = { type: 'div', key: 'C1', return: B1 }
const C2 = { type: 'div', key: 'C2', return: B1 }
const C3 = { type: 'div', key: 'C3', return: B2 }
const C4 = { type: 'div', key: 'C4', return: B2 }
A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4
module.exports = A1
Напишите метод обхода:
let rootFiber = require('./element')
const beginWork = (Fiber) => {
console.log(`${Fiber.key} start`)
}
const completeUnitWork = (Fiber) => {
console.log(`${Fiber.key} end`)
}
// 遍历函数
const performUnitOfWork = (Fiber) => {
beginWork(Fiber)
if (Fiber.child) {
return Fiber.child
}
while (Fiber) {
completeUnitWork(Fiber)
if (Fiber.sibling) {
return Fiber.sibling
}
Fiber = Fiber.return
}
}
const workloop = (nextUnitOfWork) => {
// 如果有待执行的执行单元则执行,返回下一个执行单元
while (nextUnitOfWork) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if (!nextUnitOfWork) {
console.log('reconciliation阶段结束')
}
}
workloop(rootFiber)
распечатать результат:
A1 start
B1 start
C1 start
C1 end // C1完成
C2 start
C2 end // C2完成
B1 end // B1完成
B2 start
C3 start
C3 end // C3完成
C4 start
C4 end // C4完成
B2 end // B2完成
A1 end // A1完成
reconciliation阶段结束
собрать список эффектов
Следующее, что нужно сделать, зная метод обхода, — это собрать результаты изменений всех узлов в процессе обхода.effect list
, обратите внимание, что включены только те узлы, которые необходимо изменить. Слияние в конце каждого обновления узлаeffect list
Для сбора результатов задачи последний корневой узелeffect list
Он записывает все результаты, которые необходимо изменить.
собиратьeffect list
Конкретные шаги:
- Если текущий узел необходимо обновить, нажмите
tag
Обновить текущее состояние узла (реквизиты, состояние, контекст и т. д.) - Создайте волокна для каждого дочернего узла. если не сгенерировано
child fiber
, затем завершите узел, поставьтеeffect list
слился вreturn
, поместите этот узелsibling
узел как следующий пройденный узел, иначеchild
узел как следующий пройденный узел - Если осталось время, запустите следующий узел, в противном случае дождитесь следующего момента, когда основной поток будет бездействовать, прежде чем запускать следующий узел.
- Если следующего узла нет, введите
pendingCommit
состояние, в это времяeffect list
Сбор окончен, окончен.
собиратьeffect list
Порядок обхода следующий:
Обходим массив дочерних виртуальных элементов DOM, создавая дочерние волокна для каждого виртуального элемента DOM:
const reconcileChildren = (currentFiber, newChildren) => {
let newChildIndex = 0
let prevSibling // 上一个子fiber
// 遍历子虚拟DOM元素数组,为每个虚拟DOM元素创建子fiber
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]
let tag
// 打tag,定义 fiber类型
if (newChild.type === ELEMENT_TEXT) { // 这是文本节点
tag = TAG_TEXT
} else if (typeof newChild.type === 'string') { // 如果type是字符串,则是原生DOM节点
tag = TAG_HOST
}
let newFiber = {
tag,
type: newChild.type,
props: newChild.props,
stateNode: null, // 还未创建DOM元素
return: currentFiber, // 父亲fiber
effectTag: INSERT, // 副作用标识,包括新增、删除、更新
nextEffect: null, // 指向下一个fiber,effect list通过nextEffect指针进行连接
}
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber // child为大儿子
} else {
prevSibling.sibling = newFiber // 让大儿子的sibling指向二儿子
}
prevSibling = newFiber
}
newChildIndex++
}
}
Определите метод для сбора всех побочных эффектов в этом волоконном узле и скомпонуйтеeffect list
. Обратите внимание, что каждое волокно имеет два свойства:
- firstEffect: указывает на первое дочернее волокно с побочными эффектами
- lastEffect: указывает на последнее дочернее волокно с побочными эффектами
использовать междуnextEffect
Составьте односвязный список.
// 在完成的时候要收集有副作用的fiber,组成effect list
const completeUnitOfWork = (currentFiber) => {
// 后续遍历,儿子们完成之后,自己才能完成。最后会得到以上图中的链条结构。
let returnFiber = currentFiber.return
if (returnFiber) {
// 如果父亲fiber的firstEffect没有值,则将其指向当前fiber的firstEffect
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect
}
// 如果当前fiber的lastEffect有值
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
}
returnFiber.lastEffect = currentFiber.lastEffect
}
const effectTag = currentFiber.effectTag
if (effectTag) { // 说明有副作用
// 每个fiber有两个属性:
// 1)firstEffect:指向第一个有副作用的子fiber
// 2)lastEffect:指向最后一个有副作用的子fiber
// 中间的使用nextEffect做成一个单链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber
} else {
returnFiber.lastEffect = currentFiber
}
returnFiber.lastEffect = currentFiber
}
}
}
Затем определите рекурсивную функцию, которая начинается с корневого узла, проходит через все узлы волокна и выводит все конечныеeffect list
:
// 把该节点和子节点任务都执行完
const performUnitOfWork = (currentFiber) => {
beginWork(currentFiber)
if (currentFiber.child) {
return currentFiber.child
}
while (currentFiber) {
completeUnitOfWork(currentFiber) // 让自己完成
if (currentFiber.sibling) { // 有弟弟则返回弟弟
return currentFiber.sibling
}
currentFiber = currentFiber.return // 没有弟弟,则找到父亲,让父亲完成,父亲会去找他的弟弟即叔叔
}
}
этап фиксации
Фаза фиксации должна выполнять побочные эффекты, рассчитанные на предыдущей фазе, и должна быть обработана за один раз.Эта фаза не может быть приостановлена, иначе будут прерывистые обновления пользовательского интерфейса. Этот этап требуетeffect list
, фиксирует все обновления дерева DOM.
Обновление представления на основе списка эффектов волокна
согласно волокнуeffect list
Список для обновления представления (здесь перечислены только три операции добавления узлов, удаления узлов и обновления узлов):
const commitWork = currentFiber => {
if (!currentFiber) return
let returnFiber = currentFiber.return
let returnDOM = returnFiber.stateNode // 父节点元素
if (currentFiber.effectTag === INSERT) { // 如果当前fiber的effectTag标识位INSERT,则代表其是需要插入的节点
returnDOM.appendChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === DELETE) { // 如果当前fiber的effectTag标识位DELETE,则代表其是需要删除的节点
returnDOM.removeChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === UPDATE) { // 如果当前fiber的effectTag标识位UPDATE,则代表其是需要更新的节点
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text
}
}
}
currentFiber.effectTag = null
}
Обновите представление на основе списка эффектов всех волокон.
Напишите рекурсивную функцию, начиная с корневого узла, в соответствии сeffect list
Выполните все обновления:
const commitRoot = () => {
let currentFiber = workInProgressRoot.firstEffect
while (currentFiber) {
commitWork(currentFiber)
currentFiber = currentFiber.nextEffect
}
currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot
workInProgressRoot = null
}
Полное обновление просмотра
Далее определите реализацию петли, когда расчет завершен для каждого из волокнаeffect list
После этого вызовите commitRoot для завершения обновления представления:
const workloop = (deadline) => {
let shouldYield = false // 是否需要让出控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1 // 如果执行完任务后,剩余时间小于1ms,则需要让出控制权给浏览器
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束')
commitRoot() // 没有下一个任务了,根据effect list结果批量更新视图
}
// 请求浏览器进行再次调度
requestIdleCallback(workloop, { timeout: 1000 })
}
На этом этапе операция обновления представления завершена в соответствии с собранной информацией об изменениях.
Суммировать
Эта статья должна дать вам общее представление о React Fiber.В этой статье рассказывается, почему механизм Fiber введен в React, какова его конструктивная идея и как он постепенно реализуется в коде. Но есть еще много моментов, которые не были освещены, например, как определить приоритет задач планирования, как выполнить прерывание задачи и восстановление точки останова... Заинтересованные друзья могут объединитьreactИсходный код для продолжения исследований.