Эволюция архитектуры React — от рекурсии к циклу

React.js внешний фреймворк

Эта статья является второй частью эволюции архитектуры 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.

Table

// 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.

Table Loop

// 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.

Update

Этот процесс также можно ясно увидеть через стек вызовов функций производительности.После 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' // 子节点仅有文本节点
    }
  }
}

Fiber

Реализация циклического обновления

Итак, когда 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.

Fiber work loop

При обходе каждый узел проходит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.