Цель написания этой статьи - в основном понять, что такое новейшая файбер-архитектура 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
Фактически значение пересчитывается только один раз.
После каждого 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);
}
}
}
Этот процесс также визуализируется в комментариях к исходному коду 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);
}
}
};
Запуск транзакции можно разделить на три шага:
- Сначала выполните инициализацию оболочки.На данный момент инициализация — это все пустые функции, которые можно пропустить напрямую;
- Затем выполните обратный вызов (то есть enqueueUpdate).При выполнении enqueueUpdate, поскольку он перешел в состояние обновления,
batchingStrategy.isBatchingUpdates
был изменен наtrue
, поэтому в итоге компонент все равно будет помещен в очередь грязных компонентов в ожидании обновления; - Два близких метода, выполненные позже, первый метод
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
.可以看看下面两张图:
Когда компонент смонтирован и вызывается событие, оно будет вызвано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.
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 много подробностей о разделении временных срезов и приоритете задач. будет помещено в следующую статью Другими словами, это новая яма, прежде чем вы это узнаете.