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

внешний интерфейс React.js

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

Начните с setState

Причина, по которой React 16 должен подвергнуться серьезному рефакторингу, заключается в том, что предыдущая версия React имеет некоторые неизбежные дефекты, и некоторые операции обновления необходимо изменить с синхронных на асинхронные. Итак, давайте сначала поговорим о том, как React 15 выполняет setState.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    // 第一次调用
    this.setState({ val: this.state.val + 1 });
    console.log('first setState', this.state);

    // 第二次调用
    this.setState({ val: this.state.val + 1 });
    console.log('second setState', this.state);

    // 第三次调用
    this.setState({ val: this.state.val + 1 }, () => {
      console.log('in callback', this.state)
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

Студенты, знакомые с React, должны знать, что в жизненном цикле React несколько setState будут объединены в один, хотя setState выполняется три раза подряд,state.valФактически значение пересчитывается только один раз.

render结果

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

控制台输出

В Интернете много статей о том, что setState — это «асинхронная операция», поэтому последнее значение нельзя получить после setState, на самом деле это мнение неверно. setState — это синхронная операция, но она не выполняется сразу после каждой операции, вместо этого setState кэшируется, и когда процесс монтирования завершится или завершится событийная операция, все состояния будут вынесены за один расчет. Если setState отсоединенReact 的生命周期илиReact 提供的事件流, вы можете получить результат сразу после setState.

Мы модифицируем приведенный выше код, помещаем setState в setTimeout и выполняем его в следующей очереди задач.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    setTimeout(() => {
      // 第一次调用
      this.setState({ val: this.state.val + 1 });
      console.log('first setState', this.state);
  
      // 第二次调用
      this.setState({ val: this.state.val + 1 });
      console.log('second setState', this.state);
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

Как видите, его можно увидеть сразу после setStatestate.valзначение изменилось.

控制台输出

Чтобы лучше понять setState, давайте кратко объясним логику обновления setState в React 15. Следующий код представляет собой некоторое упрощение исходного кода, а не полную логику.

Анализ исходного кода старой версии setState

Основная логика setState реализована в ReactUpdateQueue, после вызова setState состояние не модифицируется сразу, а поступающие параметры помещаются внутрь компонента._pendingStateQueue, затем позвонитеenqueueUpdateобновить.

// 对外暴露的 React.Component
function ReactComponent() {
  this.updater = ReactUpdateQueue;
}
// setState 方法挂载到原型链上
ReactComponent.prototype.setState = function (partialState, callback) {
  // 调用 setState 后,会调用内部的 updater.enqueueSetState
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    // 在组件的 _pendingStateQueue 上暂存新的 state
    if (!component._pendingStateQueue) {
      component._pendingStateQueue = [];
    }
    var queue = component._pendingStateQueue;
    queue.push(partialState);
    enqueueUpdate(component);
  },
  enqueueCallback: function (component, callback, callerName) {
    // 在组件的 _pendingCallbacks 上暂存 callback
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else {
      component._pendingCallbacks = [callback];
    }
    enqueueUpdate(component);
  }
}

enqueueUpdateсначала пройдетbatchingStrategy.isBatchingUpdatesОпределите, выполняется ли в данный момент процесс обновления. Если он не находится в процессе обновления, он будет вызванbatchingStrategy.batchedUpdates()обновить. Если он находится в процессе, обновляемый компонент будет помещен вdirtyComponentsкеш.

var dirtyComponents = [];
function enqueueUpdate(component) {
  if (!batchingStrategy.isBatchingUpdates) {
  	// 开始进行批量更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果在更新流程,则将组件放入脏组件队列,表示组件待更新
  dirtyComponents.push(component);
}

batchingStrategyЭто стратегия пакетной обработки React, реализация этой стратегии основана наTransaction, хотя имя такое же, как у транзакции базы данных, но суть другая.

class ReactDefaultBatchingStrategyTransaction extends Transaction {
  constructor() {
    this.reinitializeTransaction()
  }
  getTransactionWrappers () {
    return [
      {
        initialize: () => {},
        close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
      },
      {
        initialize: () => {},
        close: () => {
          ReactDefaultBatchingStrategy.isBatchingUpdates = false;
        }
      }
    ]
  }
}

var transaction = new ReactDefaultBatchingStrategyTransaction();

var batchingStrategy = {
  // 判断是否在更新流程中
  isBatchingUpdates: false,
  // 开始进行批量更新
  batchedUpdates: function (callback, component) {
    // 获取之前的更新状态
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
		// 将更新状态修改为 true
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // 如果已经在更新状态中,等待之前的更新结束
      return callback(callback, component);
    } else {
      // 进行更新
      return transaction.perform(callback, null, component);
    }
  }
};

TransactionНачато с помощью метода execute, а затем расширено методомgetTransactionWrappersПолучите массив объектов-оболочек, каждый из которых содержит два свойства:initialize,close. выполнить позвонит всемwrapper.initialize, затем вызовите входящий обратный вызов и, наконец, вызовите всеwrapper.close.

class Transaction {
	reinitializeTransaction() {
    this.transactionWrappers = this.getTransactionWrappers();
  }
	perform(method, scope, ...param) {
    this.initializeAll(0);
    var ret = method.call(scope, ...param);
    this.closeAll(0);
    return ret;
  }
	initializeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.initialize.call(this);
    }
  }
	closeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.close.call(this);
    }
  }
}

transaction.perform

Этот процесс также визуализируется в комментариях к исходному коду React.

/*
*                       wrappers (injected at creation time)
*                                      +        +
*                                      |        |
*                    +-----------------|--------|--------------+
*                    |                 v        |              |
*                    |      +---------------+   |              |
*                    |   +--|    wrapper1   |---|----+         |
*                    |   |  +---------------+   v    |         |
*                    |   |          +-------------+  |         |
*                    |   |     +----|   wrapper2  |--------+   |
*                    |   |     |    +-------------+  |     |   |
*                    |   |     |                     |     |   |
*                    |   v     v                     v     v   | wrapper
*                    | +---+ +---+   +---------+   +---+ +---+ | invariants
* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | +---+ +---+   +---------+   +---+ +---+ |
*                    |  initialize                    close    |
*                    +-----------------------------------------+
*/

Давайте упростим код и вернемся к потоку setState.

// 1. 调用 Component.setState
ReactComponent.prototype.setState = function (partialState) {
  this.updater.enqueueSetState(this, partialState);
};

// 2. 调用 ReactUpdateQueue.enqueueSetState,将 state 值放到 _pendingStateQueue 进行缓存
var ReactUpdateQueue = {
  enqueueSetState(component, partialState) {
    var queue = component._pendingStateQueue || (component._pendingStateQueue = []);
    queue.push(partialState);
    enqueueUpdate(component);
  }
}

// 3. 判断是否在更新过程中,如果不在就进行更新
var dirtyComponents = [];
function enqueueUpdate(component) {
  // 如果之前没有更新,此时的 isBatchingUpdates 肯定是 false
  if (!batchingStrategy.isBatchingUpdates) {
    // 调用 batchingStrategy.batchedUpdates 进行更新
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
}

// 4. 进行更新,更新逻辑放入事务中进行处理
var batchingStrategy = {
  isBatchingUpdates: false,
  // 注意:此时的 callback 为 enqueueUpdate 
  batchedUpdates: function (callback, component) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    if (alreadyBatchingUpdates) {
      // 如果已经在更新状态中,重新调用 enqueueUpdate,将 component 放入 dirtyComponents
      return callback(callback, component);
    } else {
      // 进行事务操作
      return transaction.perform(callback, null, component);
    }
  }
};

Запуск транзакции можно разделить на три шага:

  1. Сначала выполните инициализацию оболочки.На данный момент инициализация — это все пустые функции, которые можно пропустить напрямую;
  2. Затем выполните обратный вызов (то есть enqueueUpdate).При выполнении enqueueUpdate, поскольку он перешел в состояние обновления,batchingStrategy.isBatchingUpdatesбыл изменен наtrue, поэтому в итоге компонент все равно будет помещен в очередь грязных компонентов в ожидании обновления;
  3. Два близких метода, выполненные позже, первый методflushBatchedUpdatesОн используется для обновления компонента, а второй метод используется для изменения статуса обновления, указывающего на то, что обновление завершено.
getTransactionWrappers () {
  return [
    {
      initialize: () => {},
      close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
    },
    {
      initialize: () => {},
      close: () => {
        ReactDefaultBatchingStrategy.isBatchingUpdates = false;
      }
    }
  ]
}

flushBatchedUpdatesОн удалит все грязные очереди компонентов для сравнения и, наконец, обновит их до DOM.

function flushBatchedUpdates() {
  if (dirtyComponents.length) {
    runBatchedUpdates()
  }
};

function runBatchedUpdates() {
  // 省略了一些去重和排序的操作
  for (var i = 0; i < dirtyComponents.length; i++) {
    var component = dirtyComponents[i];

    // 判断组件是否需要更新,然后进行 diff 操作,最后更新 DOM。
    ReactReconciler.performUpdateIfNecessary(component);
  }
}

performUpdateIfNecessary()позвонюComponent.updateComponent(),существуетupdateComponent(), будет из_pendingStateQueueВыньте все значения для обновления.

// 获取最新的 state
_processPendingState() {
  var inst = this._instance;
  var queue = this._pendingStateQueue;

  var nextState = { ...inst.state };
  for (var i = 0; i < queue.length; i++) {
    var partial = queue[i];
    Object.assign(
      nextState,
      typeof partial === 'function' ? partial(inst, nextState) : partial
   );
  }
  return nextState;
}
// 更新组件
updateComponent(prevParentElement, nextParentElement) {
  var inst = this._instance;
  var prevProps = prevParentElement.props;
  var nextProps = nextParentElement.props;
  var nextState = this._processPendingState();
  var shouldUpdate = 
      !shallowEqual(prevProps, nextProps) ||
      !shallowEqual(inst.state, nextState);
  
  if (shouldUpdate) {
    // diff 、update DOM
  } else {
    inst.props = nextProps;
    inst.state = nextState;
  }
  // 后续的操作包括判断组件是否需要更新、diff、更新到 DOM
}

Причина слияния setState

Согласно только что объясненной логике, когда setState,batchingStrategy.isBatchingUpdatesдляfalseБудет открыта транзакция, компонент будет помещен в очередь грязных компонентов, и, наконец, будет выполнена операция обновления, и все это синхронные операции. Честно говоря, после setState мы можем сразу получить последнее состояние.

Однако это не так, в жизненном цикле React и его потоке событийbatchingStrategy.isBatchingUpdatesЗначение было изменено рано.true.可以看看下面两张图:

Mount

事件调用

Когда компонент смонтирован и вызывается событие, оно будет вызваноbatchedUpdates, транзакция в это время уже началась, поэтому, пока она не покинет React, независимо от того, сколько раз используется setState, ее компоненты будут помещены в очередь грязных компонентов для ожидания обновления. Выйдя из-под контроля React, например, в setTimeout, setState сразу же становится игрой с одним человеком.

Параллельный режим

Архитектура Fiber, представленная React 16, должна проложить путь для последующих возможностей асинхронного рендеринга.Хотя архитектура была изменена, возможности асинхронного рендеринга официально не запущены, и мы можем использовать их только в экспериментальной версии. Асинхронный рендеринг относится кПараллельный режим, ниже приводится введение официального сайта:

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

优点

В дополнение к параллельному режиму React также предоставляет два других режима: устаревший режим по-прежнему является методом синхронного обновления, который можно рассматривать как режим совместимости, соответствующий старой версии, а режим блокировки — это переходная версия.

模式差异

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

Браузер является однопоточным, он объединяет рендеринг графического интерфейса, обработку таймера, обработку событий, выполнение JS и удаленную загрузку ресурсов. Когда одна вещь сделана, следующая может быть сделана только тогда, когда она сделана. Если времени достаточно, браузер скомпилирует и оптимизирует наш код (JIT) и выполнит горячую оптимизацию кода, а некоторые операции DOM также обработают перекомпоновку внутри. Перекомпоновка — это черная дыра производительности, которая может привести к изменению макета большинства элементов страницы.

Как работает браузер:渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> ....

Некоторые из этих задач управляемые, а некоторые неуправляемые, например, трудно сказать, когда выполняется setTimeout, он всегда не вовремя, время загрузки ресурса неконтролируемо. Но мы можем контролировать некоторые JS и позволить им диспетчеризировать выполнение, продолжительность задач не должна быть слишком большой, чтобы браузер успел оптимизировать код JS и исправить перекомпоновку!

Подводить итоги,Просто дайте браузеру отдохнуть, браузер может работать быстрее.

-- Ситу Чжэнмэй«Архитектура React Fiber»

模式差异

Вот демонстрация, выше — анимация, которая вращается вокруг ☀️, а внизу — заданное по времени setState React для обновления представления.В синхронном режиме каждое setState вызывает зависание вышеуказанной анимации, в то время как анимация в асинхронном режиме очень плавная. .

Режим синхронизации:

同步模式

Асинхронный режим:

异步模式

как использовать

Хотя во многих статьях рассказывается о параллельном режиме, эта возможность на самом деле не доступна в сети, и вы можете установить экспериментальную версию, только если хотите ее использовать. Вы также можете напрямую передать этот cdn:UN PK Geng.com/browse/Hot Eat….

npm install react@experimental react-dom@experimental

Если вы хотите включить параллельный режим, вы не можете использовать предыдущийReactDOM.render, необходимо заменить наReactDOM.createRoot, а в экспериментальной версии, т.к. API недостаточно стабилен, нужно пройтиReactDOM.unstable_createRootчтобы включить параллельный режим.

import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.unstable_createRoot(
  document.getElementById('root')
).render(<App />);

Обновление слияния setState

Помните, что в предыдущем случае с React15 setState выполнялся в setTimeout,state.valсразу изменится. Тот же самый код, мы получаем однократный запуск режима Concurrent.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  componentDidMount() {
    setTimeout(() => {
      // 第一次调用
      this.setState({ val: this.state.val + 1 });
      console.log('first setState', this.state);
  
      // 第二次调用
      this.setState({ val: this.state.val + 1 });
      console.log('second setState', this.state);
      
      this.setState({ val: this.state.val + 1 }, () => {
        console.log(this.state);
      });
    });
  }
  render() {
    return <div> val: { this.state.val } </div>
  }
}

export default App;

控制台输出

Это показывает, что в режиме Concurrent setState все еще может объединяться и обновляться, даже если он находится вне жизненного цикла React. Основная причина в том, что в режиме Concurrent реальная операция обновления перемещается в следующую очередь событий, подобно nextTick в Vue.

Изменения механизма обновления

Давайте изменим демо и посмотрим на стек вызовов после нажатия кнопки.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  clickBtn() {
    this.setState({ val: this.state.val + 1 });
  }
  render() {
    return (<div>
      <button onClick={() => {this.clickBtn()}}>click add</button>
      <div>val: { this.state.val }</div>
    </div>)
  }
}

export default App;

调用栈

调用栈

onClickПосле срабатывания выполнить операцию setState, а затем вызвать метод enqueueState, вроде как и предыдущий режим, но последующие операции принципиально изменились, потому что в React 16 нет транзакции.

Component.setState() => enquueState() => scheduleUpdate() => scheduleCallback()
=> requestHostCallback(flushWork) => postMessage()

Настоящая асинхронная логика находится вrequestHostCallback,postMessageВнутри это планировщик, который React реализует сам:GitHub.com/Facebook/Горячие….

function unstable_scheduleCallback(priorityLevel, calback) {
  var currentTime = getCurrentTime();
  var startTime = currentTime + delay;
  var newTask = {
    id: taskIdCounter++,
    startTime: startTime,           // 任务开始时间
    expirationTime: expirationTime, // 任务终止时间
    priorityLevel: priorityLevel,   // 调度优先级
    callback: callback,             // 回调函数
  };
  if (startTime > currentTime) {
    // 超时处理,将任务放到 taskQueue,下一个时间片执行
    // 源码中其实是 timerQueue,后续会有个操作将 timerQueue 的 task 转移到 taskQueue
  	push(taskQueue, newTask)
  } else {
    requestHostCallback(flushWork);
  }
  return newTask;
}

Реализация requestHostCallback зависит отMessageChannel, но MessageChannel здесь используется не для обмена сообщениями, а для использования его асинхронных возможностей, чтобы дать браузеру возможность дышать. Говоря о MessageChannel, nextTick Vue 2.5 также использовался, но он был отменен, когда был выпущен 2.6.

vue@2.5

MessageChannel предоставляет два объекта,port1а такжеport2,port1Отправленные сообщения могут бытьport2получить, то же самоеport2Отправленные сообщения также могут бытьport1Получить, только время для получения сообщения будет помещено в следующую макрозадачу.

var { port1, port2 } = new MessageChannel();
// port1 接收 port2 的消息
port1.onmessage = function (msg) { console.log('MessageChannel exec') }
// port2 发送消息
port2.postMessage(null)

new Promise(r => r()).then(() => console.log('promise exec'))
setTimeout(() => console.log('setTimeout exec'))

console.log('start run')

执行结果

можно увидеть,port1Время получения сообщения позже микрозадачи, в которой находится промис, но раньше, чем setTimeout. React использует эту возможность, чтобы дать браузеру передышку, чтобы не умереть с голоду.

В предыдущем случае синхронное обновление не давало браузеру передышки, вызывая зависание представления.

同步更新

При асинхронном обновлении квант времени разделяется, что дает браузеру достаточно времени для обновления анимации.

异步更新

Вернемся к уровню кода и посмотрим, как React использует MessageChannel.

var isMessageLoopRunning = false; // 更新状态
var scheduledHostCallback = null; // 全局的回调
var channel = new MessageChannel();
var port = channel.port2;

channel.port1.onmessage = function () {
  if (scheduledHostCallback !== null) {
    var currentTime = getCurrentTime();
    // 重置超时时间
    deadline = currentTime + yieldInterval;
    var hasTimeRemaining = true;

    // 执行 callback
    var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);

    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);
  }
};

Посмотрите еще раз на обратный вызов, переданный ранее (flushWork),передачаworkLoop, вынесите выполнение задачи в taskQueue.

// 精简了相当多的代码
function flushWork(hasTimeRemaining, initialTime) {
  return workLoop(hasTimeRemaining, initialTime);
}

function workLoop(hasTimeRemaining, initialTime) {
  var currentTime = initialTime;
  // scheduleCallback 进行了 taskQueue 的 push 操作
  // 这里是获取之前时间片未执行的操作
  currentTask = peek(taskQueue);

  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTime) {
      // 超时需要中断任务
      break;
    }

    currentTask.callback();         // 执行任务回调
    currentTime = getCurrentTime(); // 重置当前时间
    currentTask = peek(taskQueue);  // 获取新的任务
  }
	// 如果当前任务不为空,表明是超时中断,返回 true
  if (currentTask !== null) {
    return true;
  } else {
    return false;
  }
}

Видно, что React использует expireTime, чтобы определить, истекло ли время ожидания, и если оно истекло, задача будет выполнена позже. Поэтому, когда setState выполняется в setTimeout в асинхронной модели, пока текущий квант времени не заканчивается (currentTime меньше, чем expireTime), несколько setState все еще могут быть объединены в один.

Далее давайте проведем еще один эксперимент: в setTimeout setState выполняется 500 раз подряд, чтобы посмотреть, сколько раз он наконец подействует.

import React from 'react';

class App extends React.Component {
  state = { val: 0 }
  clickBtn() {
    for (let i = 0; i < 500; i++) {
      setTimeout(() => {
        this.setState({ val: this.state.val + 1 });
      })
    }
  }
  render() {
    return (<div>
      <button onClick={() => {this.clickBtn()}}>click add</button>
      <div>val: { this.state.val }</div>
    </div>)
  }
}

export default App;

Сначала посмотрите на синхронный режим:

同步模式

Посмотрите еще раз на асинхронный режим:

异步模式

Последнее значение setState равно 81 раз, что указывает на то, что здесь операция выполняется в рамках 81 кванта времени, и каждый квант времени обновляется один раз.

Суммировать

Проходит много времени до и после этой статьи.Читать исходный код React действительно больно, потому что я не разбирался в нем раньше.Я просто сначала читал анализ некоторых статей, но там много неясностей, поэтому Отлаживать могу только на исходниках, и то разово.Прочитав код двух версий React 15 и 16, чувствую, что мозгов не хватает.

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