Эта статья является второй частью эволюции архитектуры React.Предыдущая статья в основном представила изменение механизма обновления с синхронного на асинхронный.Эта статья посвящена процессу обновления посредством циклического обхода в архитектуре Fiber.Причина использования метод циклического обхода. Это связано с тем, что процесс рекурсивного обновления нельзя приостановить после его запуска, и он может останавливаться только до тех пор, пока рекурсия не завершится или не возникнет исключение.
Реализация рекурсивного обновления
Логика рекурсивного обновления React 15 заключается в том, чтобы сначала поместить компоненты, которые необходимо обновить, в очередь грязных компонентов (это было представлено в предыдущей статье, если вы еще не видели, вы можете сначала посмотретьЭволюция архитектуры React — от синхронной к асинхронной), затем извлеките компонент для рекурсии и продолжайте искать дочерние узлы, чтобы узнать, нужно ли его обновлять.
Далее используется фрагмент кода для краткого описания процесса:
updateComponent (prevElement, nextElement) {
if (
// 如果组件的 type 和 key 都没有发生变化,进行更新
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
) {
// 文本节点更新
if (prevElement.type === 'text') {
if (prevElement.value !== nextElement.value) {
this.replaceText(nextElement.value)
}
}
// DOM 节点的更新
else {
// 先更新 DOM 属性
this.updateProps(prevElement, nextElement)
// 再更新 children
this.updateChildren(prevElement, nextElement)
}
}
// 如果组件的 type 和 key 发生变化,直接重新渲染组件
else {
// 触发 unmount 生命周期
ReactReconciler.unmountComponent(prevElement)
// 渲染新的组件
this._instantiateReactComponent(nextElement)
}
},
updateChildren (prevElement, nextElement) {
var prevChildren = prevElement.children
var nextChildren = nextElement.children
// 省略通过 key 重新排序的 diff 过程
if (prevChildren === null) { } // 渲染新的子节点
if (nextChildren === null) { } // 清空所有子节点
// 子节点对比
prevChildren.forEach((prevChild, index) => {
const nextChild = nextChildren[index]
// 递归过程
this.updateComponent(prevChild, nextChild)
})
}
Чтобы увидеть этот процесс более наглядно, мы все же напишем простую демонстрацию и построим компонент таблицы 3*3.
// https://codesandbox.io/embed/react-sync-demo-nlijf
class Col extends React.Component {
render() {
// 渲染之前暂停 8ms,给 render 制造一点点压力
const start = performance.now()
while (performance.now() - start < 8)
return <td>{this.props.children}</td>
}
}
export default class Demo extends React.Component {
state = {
val: 0
}
render() {
const { val } = this.state
const array = Array(3).fill()
// 构造一个 3 * 3 表格
const rows = array.map(
(_, row) => <tr key={row}>
{array.map(
(_, col) => <Col key={col}>{val}</Col>
)}
</tr>
)
return (
<table className="table">
<tbody>{rows}</tbody>
</table>
)
}
}
Затем обновляйте значение в таблице каждую секунду, пусть val + 1 каждый раз и цикл от 0 до 9.
// https://codesandbox.io/embed/react-sync-demo-nlijf
export default class Demo extends React.Component {
tick = () => {
setTimeout(() => {
this.setState({ val: next < 10 ? next : 0 })
this.tick()
}, 1000)
}
componentDidMount() {
this.tick()
}
}
Полный код онлайн-адреса:Код Sandbox.io/embed/react .... Каждый раз, когда демонстрационный компонент вызывает setState, React сначала определяет, был ли изменен тип компонента. Если да, он повторно отображает весь компонент. Если нет, он обновляет состояние, а затем оценивает табличный компонент вниз. , и табличный компонент будет продолжать оценивать компонент tr в сторону понижения. Затем компонент tr оценивает компонент td в сторону понижения и, наконец, обнаруживает, что текстовый узел под компонентом td был изменен и обновлен с помощью DOM API.
Этот процесс также можно ясно увидеть через стек вызовов функций производительности.После updateComponent updateChildren будет продолжать вызывать updateComponent подкомпонентов до тех пор, пока все компоненты не будут рекурсивно завершены, указывая на то, что обновление завершено.
Недостаток рекурсии очевиден. Вы не можете приостановить обновление. После запуска вы должны начать с начала и до конца. Это явно несовместимо с концепцией разделения временных срезов в React 16 и предоставления браузеру передышки. Поэтому React должен переключить архитектуру и изменить виртуальный DOM с древовидной структуры на структуру связанного списка.
Перерабатываемое волокно
Структура связанного списка, упомянутая здесь, — это Fiber.Самое большое преимущество структуры связанного списка заключается в том, что ее можно проходить в цикле.Пока вы помните текущую пройденную позицию, вы можете быстро восстановить и перезапустить обход даже после прерывания.
Давайте посмотрим на структуру данных волокна узла:
function FiberNode (tag, key) {
// 节点 key,主要用于了优化列表 diff
this.key = key
// 节点类型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
this.tag = tag
// 子节点
this.child = null
// 父节点
this.return = null
// 兄弟节点
this.sibling = null
// 更新队列,用于暂存 setState 的值
this.updateQueue = null
// 节点更新过期时间,用于时间分片
// react 17 改为:lanes、childLanes
this.expirationTime = NoLanes
this.childExpirationTime = NoLanes
// 对应到页面的真实 DOM 节点
this.stateNode = null
// Fiber 节点的副本,可以理解为备胎,主要用于提升更新的性能
this.alternate = null
}
Вот пример, где у нас есть кусок обычного HTML-текста:
<table class="table">
<tr>
<td>1</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
</tr>
</table>
В предыдущих версиях React jsx был преобразован в метод createElement, который создавал виртуальный DOM с древовидной структурой.
const VDOMRoot = {
type: 'table',
props: { className: 'table' },
children: [
{
type: 'tr',
props: { },
children: [
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
},
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
}
]
},
{
type: 'tr',
props: { },
children: [
{
type: 'td',
props: { },
children: [{type: 'text', value: '1'}]
}
]
}
]
}
В архитектуре Fiber структура выглядит следующим образом:
// 有所简化,并非与 React 真实的 Fiber 结构一致
const FiberRoot = {
type: 'table',
return: null,
sibling: null,
child: {
type: 'tr',
return: FiberNode, // table 的 FiberNode
sibling: {
type: 'tr',
return: FiberNode, // table 的 FiberNode
sibling: null,
child: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: null,
child: null,
text: '1' // 子节点仅有文本节点
},
child: null,
text: '1' // 子节点仅有文本节点
}
},
child: {
type: 'td',
return: FiberNode, // tr 的 FiberNode
sibling: null,
child: null,
text: '1' // 子节点仅有文本节点
}
}
}
Реализация циклического обновления
Итак, когда setState, как React выполняет обход волокна?
let workInProgress = FiberRoot
// 遍历 Fiber 节点,如果时间片时间用完就停止遍历
function workLoopConcurrent() {
while (
workInProgress !== null &&
!shouldYield() // 用于判断当前时间片是否到期
) {
performUnitOfWork(workInProgress)
}
}
function performUnitOfWork() {
const next = beginWork(workInProgress) // 返回当前 Fiber 的 child
if (next) { // child 存在
// 重置 workInProgress 为 child
workInProgress = next
} else { // child 不存在
// 向上回溯节点
let completedWork = workInProgress
while (completedWork !== null) {
// 收集副作用,主要是用于标记节点是否需要操作 DOM
completeWork(completedWork)
// 获取 Fiber.sibling
let siblingFiber = workInProgress.sibling
if (siblingFiber) {
// sibling 存在,则跳出 complete 流程,继续 beginWork
workInProgress = siblingFiber
return;
}
completedWork = completedWork.return
workInProgress = completedWork
}
}
}
function beginWork(workInProgress) {
// 调用 render 方法,创建子 Fiber,进行 diff
// 操作完毕后,返回当前 Fiber 的 child
return workInProgress.child
}
function completeWork(workInProgress) {
// 收集节点副作用
}
Обход Fiber представляет собой цикл, и существует глобальныйworkInProgress
Переменная, используемая для хранения узла, который в настоящее время отличается, сначала черезbeginWork
Затем метод выполняет операцию diff на текущем узле (рендеринг будет вызываться перед diff для пересчета состояния и свойства) и возвращает первый дочерний узел текущего узла (fiber.child
) в качестве нового рабочего узла, пока не перестанут существовать дочерние узлы. Затем вызовите текущий узелcompletedWork
метод, магазинbeginWork
Побочные эффекты, возникающие во время процесса, если у текущего узла есть одноуровневые узлы (fiber.sibling
), затем измените рабочий узел на родственный узел и повторно введитеbeginWork
обработать. доcompletedWork
Снова вернитесь к корневому узлу и выполнитеcommitRoot
Реагируйте на все побочные эффекты в реальном DOM.
При обходе каждый узел проходитbeginWork
,completeWork
, пока не вернетесь к корневому узлу, и, наконец,commitRoot
Отправьте все обновления, вы можете увидеть содержание этой части:«Демистификация технологии React».
Секрет разделения времени
Как упоминалось ранее, обход структуры Fiber поддерживает восстановление прерывания, чтобы наблюдать за этим процессом, мы изменили предыдущие компоненты 3 * 3 Table на Concurrent режим и онлайн-адрес:код sandbox.IO/embed/реагировать…. Поскольку каждый вызов части рендеринга компонента Col занимает 8 мс, что превышает временной интервал, каждая часть td приостанавливается один раз.
class Col extends React.Component {
render() {
// 渲染之前暂停 8ms,给 render 制造一点点压力
const start = performance.now();
while (performance.now() - start < 8);
return <td>{this.props.children}</td>
}
}
В этом компоненте 3*3 всего 9 компонентов Col, поэтому трудоемких задач будет 9, которые разбросаны по 9 временным срезам.Конкретную ситуацию можно увидеть через стек вызовов Performance:
В неконкурентном режиме обход узла Fiber выполняется единовременно, и он не будет разделен на несколько временных срезов, отличие состоит в том, что обход вызывается во время обхода.workLoopSync
метод, который не определяет, израсходован ли квант времени.
// 遍历 Fiber 节点
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress)
}
}
Из вышеприведенного анализа видно, чтоshouldYield
Метод определяет, был ли израсходован текущий квант времени, что также является ключом к определению того, будет ли React выполнять синхронный или асинхронный рендеринг. Если убрать понятие приоритета задачи,shouldYield
Способ можно сказать очень простой, то есть определить, не превысило ли текущее время заданноеdeadline
.
function getCurrentTime() {
return performance.now()
}
function shouldYield() {
// 获取当前时间
var currentTime = getCurrentTime()
return currentTime >= deadline
}
deadline
Как ты получил это? Вы можете просмотреть предыдущую статью (Эволюция архитектуры React — от синхронной к асинхронной) упомянутый ChannelMessage, который будет передан при запуске обновленияrequestHostCallback
(который:port2.send
) отправляет асинхронное сообщение, вperformWorkUntilDeadline
(который:port1.onmessage
) для получения сообщений.performWorkUntilDeadline
Каждый раз, когда сообщение получено, это означает, что оно попало в следующую очередь задач, и оно будет обновлено в это время.deadline
.
var channel = new MessageChannel()
var port = channel.port2
channel.port1.onmessage = function performWorkUntilDeadline() {
if (scheduledHostCallback !== null) {
var currentTime = getCurrentTime()
// 重置超时时间
deadline = currentTime + yieldInterval
var hasTimeRemaining = true
var hasMoreWork = scheduledHostCallback()
if (!hasMoreWork) {
// 已经没有任务了,修改状态
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有任务,放到下个任务队列执行,给浏览器喘息的机会
port.postMessage (null);
}
} else {
isMessageLoopRunning = false;
}
}
requestHostCallback = function (callback) {
//callback 挂载到 scheduledHostCallback
scheduledHostCallback = callback
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
// 推送消息,下个队列队列调用 callback
port.postMessage (null)
}
}
Настройка времени ожидания заключается в добавлении времени на основе текущего времени.yieldInterval
, этоyieldInterval
значение по умолчанию равно 5 мс.
deadline = currentTime + yieldInterval
В то же время React также предоставляет модификацииyieldInterval
Конкретное время кадра (единица измерения: мс) определяется путем указания частоты кадров вручную. Чем выше частота кадров, тем короче временной интервал и тем выше требования к производительности устройства.
forceFrameRate = function (fps) {
if (fps < 0 || fps > 125) {
// 帧率仅支持 0~125
return
}
if (fps > 0) {
// 一般 60 fps 的设备
// 一个时间分片的时间为 Math.floor(1000/60) = 16
yieldInterval = Math.floor(1000 / fps)
} else {
// reset the framerate
yieldInterval = 5
}
}
Суммировать
Ниже мы объединяем асинхронную логику, циклические обновления и разделение времени. Давайте рассмотрим предыдущую статью, последовательность вызова после setState в Concurrent режиме:
Component.setState()
=> enqueueSetState()
=> scheduleUpdate()
=> scheduleCallback(performConcurrentWorkOnRoot)
=> requestHostCallback()
=> postMessage()
=> performWorkUntilDeadline()
scheduleCallback
Метод будет передавать входящий обратный вызов (performConcurrentWorkOnRoot
) собраны в задачу вtaskQueue
, затем позвонитеrequestHostCallback
Отправьте сообщение, введите асинхронную задачу.performWorkUntilDeadline
Получено асинхронное сообщение отtaskQueue
Выньте задачу и начните выполнение, задача здесь та, которая была передана доperformConcurrentWorkOnRoot
метод, который в конечном итоге будет называтьсяworkLoopConcurrent
(workLoopConcurrent
Это было введено ранее, и это не будет повторяться). еслиworkLoopConcurrent
прерывается по тайм-ауту,hasMoreWork
возвращает истину черезpostMessage
Отправить сообщение, отложив операцию до следующей очереди задач.
На этом весь процесс закончился. Надеюсь, вы что-то для себя почерпнете после прочтения статьи. В следующей статье будет представлена реализация хуков под архитектуру Fiber.