当前位置: 首页 > news >正文

React18 setState是同步还是异步?

相信大家对于react的setState肯定是不陌生了, 这是一个用于更新状态的函数. 但是在之前有一道非常经典的面试题就是关于setState是同步还是异步的问题, 具体可以参考我之前写的一篇文章: 一篇文章彻底理解setState是同步还是异步!. 对于react 18之前的版本, 上文说的东西确实没错, 但是react团队已经在18中对批处理的行为做了更改, 会尽可能的将所有能进行批处理的内容都进行批处理, 以获取更好的性能, 今天我们就来聊聊react 18中对这一行为做了哪些更改.

先看现象, 再说结论

我们都用下面这段代码在不同版本的react上进行测试

class App extends React.Component {state = {data: 1}test = () => {setTimeout(() => {this.setState({data: 2});console.log('data', this.state.data);this.setState({data: 3});console.log('data', this.state.data);}, 0);}render() {console.log("render");return (<div><button onClick={this.test}>{this.state.data}</button></div>)}
}

大家觉得输出的data值会是什么, 这个render又会打印几次?

在react 17.x下, 我们能够同步的获取到data的值, 所以输出会是2, 3.同时也会经历两次react的整个render过程, 所以也会导致render被打印两次, 这都是因为setTimeout带来的影响, 具体的解释可以参考之前的文章.

而在最新版的react 18上, 这两个setData也会被异步的批处理, 合并为一次进行更新, 所以我们拿到的值始终是1, render函数也只会被打印一次.

react 18做了什么修改

官方的说明在这里: https://github.com/reactwg/react-18/discussions/21

总结一下就是:

从react 18开始, 使用了createRoot创建应用后, 所有的更新都会自动进行批处理(也就是异步合并).使用render的应用会保持之前的行为.

如果你想保持同步更新行为, 可以使用ReactDOM.flushSync().

// 新版
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const container = document.getElementById('root');
// Create a root.
const root = ReactDOM.createRoot(container);
// Render the top component to the root.
root.render(<App />);// 旧版
// import React from 'react';
// import ReactDOM from 'react-dom';
// import './index.css';
// import App from './App';
// import reportWebVitals from './reportWebVitals';// ReactDOM.render(
//   <React.StrictMode>
//     <App />
//   </React.StrictMode>,
//   document.getElementById('root')
// );// // If you want to start measuring performance in your app, pass a function
// // to log results (for example: reportWebVitals(console.log))
// // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals();

至于为什么会这样, 我们直接从源码入手就好了.

我们知道, react的setState最终会走到scheduleUpdateOnFiber来进行更新, 之前最关键的一段代码就在这里

// react 17.x
if (executionContext === NoContext) {// Flush the synchronous work now, unless we're already working or inside// a batch. This is intentionally inside scheduleUpdateOnFiber instead of// scheduleCallbackForFiber to preserve the ability to schedule a callback// without immediately flushing it. We only do this for user-initiated// updates, to preserve historical behavior of legacy mode.resetRenderTimer();flushSyncCallbackQueue();
}

executionContext代表了react当前的调度状态, 如果退出了react的调度这个值就会重新变成NoContext. 也就是说, 如果你调用setState的时候并不处于react的调度状态中, 那么就会同步的去执行你的setState.这也是为什么一旦我们使用一些异步操作就会导致setState变成同步的原因, 而在react 18中这段代码变成了这样

// react 18.x
if (lane === SyncLane && executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode && // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.!( ReactCurrentActQueue$1.isBatchingLegacy)) {// Flush the synchronous work now, unless we're already working or inside// a batch. This is intentionally inside scheduleUpdateOnFiber instead of// scheduleCallbackForFiber to preserve the ability to schedule a callback// without immediately flushing it. We only do this for user-initiated// updates, to preserve historical behavior of legacy mode.resetRenderTimer();flushSyncCallbacksOnlyInLegacyMode();
}

可以看到我们多出了好几个判断条件, 除了之前的 executionContext === NoContext 之外, 还多了三个判断条件, 我们一个一个来看看.

lane === SyncLane

这个其实没什么好说的, lane是react中和优先级有关的概念, 从函数命名就可以看出, 只有是同步任务才会进到这个if.

var TotalLanes = 31;
var NoLanes =
/*                        */
0;
var NoLane =
/*                          */
0;
var SyncLane =
/*                        */
1;
var InputContinuousHydrationLane =
/*    */
2;
var InputContinuousLane =
/*            */
4;
var DefaultHydrationLane =
/*            */
8;
var DefaultLane =
/*                    */
16;
var TransitionHydrationLane =
/*                */
32;
var TransitionLanes =
/*                       */
4194240;
var TransitionLane1 =
/*                        */
64;
var TransitionLane2 =
/*                        */
128;
var TransitionLane3 =
/*                        */
256;
var TransitionLane4 =
/*                        */
512;
var TransitionLane5 =
/*                        */
1024;
var TransitionLane6 =
/*                        */
2048;
var TransitionLane7 =
/*                        */
4096;
var TransitionLane8 =
/*                        */
8192;
var TransitionLane9 =
/*                        */
16384;
var TransitionLane10 =
/*                       */
32768;
var TransitionLane11 =
/*                       */
65536;
var TransitionLane12 =
/*                       */
131072;
var TransitionLane13 =
/*                       */
262144;
var TransitionLane14 =
/*                       */
524288;
var TransitionLane15 =
/*                       */
1048576;
var TransitionLane16 =
/*                       */
2097152;
var RetryLanes =
/*                            */
130023424;
var RetryLane1 =
/*                             */
4194304;
var RetryLane2 =
/*                             */
8388608;
var RetryLane3 =
/*                             */
16777216;
var RetryLane4 =
/*                             */
33554432;
var RetryLane5 =
/*                             */
67108864;
var SomeRetryLane = RetryLane1;
var SelectiveHydrationLane =
/*          */
134217728;
var NonIdleLanes =
/*                                 */
268435455;
var IdleHydrationLane =
/*               */
268435456;
var IdleLane =
/*                       */
536870912;
var OffscreenLane =
/*                   */
1073741824; // This function is used for the experimental timeline (react-devtools-timeline)
// It should be kept in sync with the Lanes values above.

这一堆东西就代表了react内部各种不同优先级的任务.

(fiber.mode & ConcurrentMode) === NoMode

react fiber的mode属性代表了不同的渲染模式

var NoMode =
/*                         */
0; // TODO: Remove ConcurrentMode by reading from the root tag insteadvar ConcurrentMode =
/*                 */
1;
var ProfileMode =
/*                    */
2;
var DebugTracingMode =
/*               */
4;
var StrictLegacyMode =
/*               */
8;
var StrictEffectsMode =
/*              */
16;

具体是什么意思呢, 比如说ProfileMode代表了性能调试模式, 我们的开发环境的mode就会被赋予这个值, 可以在控制台中给开发者输出一些提示信息.而其他的值是啥意思呢..其实我目前也不能很明白的给大家说清楚, 因为我也没研究过, 但是只要知道这是个区分不同模式的变量就行了.

而这个值会在什么地方赋给fiber的mode属性呢, 会在我们整个应用的入口, 然后经过一系列的函数调用, 最终会走到createHostRootFiber方法

function createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride) {var mode;if (tag === ConcurrentRoot) {mode = ConcurrentMode;if (isStrictMode === true) {mode |= StrictLegacyMode;{mode |= StrictEffectsMode;}}} else {mode = NoMode;}if ( isDevToolsPresent) {// Always collect profile timings when DevTools are present.// This enables DevTools to start capturing timing at any point–// Without some nodes in the tree having empty base times.mode |= ProfileMode;}return createFiber(HostRoot, null, null, mode);
}

其中tag的值就是根据入口函数一路传下来的. 我们之前提到, 要想体验自动批处理需要在应用入口将ReactDOM.render替换为ReactDOM.createRoot, 这两者有什么区别呢:

function render(element, container, callback) {// 省略...return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
}function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {// 省略...if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);fiberRoot = root;// 省略...} else {// 省略...}return getPublicRootInstance(fiberRoot);
}function legacyCreateRootFromDOMContainer(container, forceHydrate) {// 省略...var root = createContainer(container, LegacyRoot, forceHydrate, null, // hydrationCallbacksfalse, // isStrictModefalse, // concurrentUpdatesByDefaultOverride,'' // identiferPrefix);// 省略...return root;
}function createContainer(containerInfo, tag, hydrate, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix) {return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix);
}function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix) {// 省略...var uninitializedFiber = createHostRootFiber(tag, isStrictMode);root.current = uninitializedFiber;uninitializedFiber.stateNode = root;// 省略...return root;
}function createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride) {var mode;if (tag === ConcurrentRoot) {mode = ConcurrentMode;if (isStrictMode === true) {mode |= StrictLegacyMode;{mode |= StrictEffectsMode;}}} else {mode = NoMode;}if ( isDevToolsPresent) {// Always collect profile timings when DevTools are present.// This enables DevTools to start capturing timing at any point–// Without some nodes in the tree having empty base times.mode |= ProfileMode;}return createFiber(HostRoot, null, null, mode);
}

我这里把从ReactDOM.render到createHostRootFiber的全部函数都放在了这里, 可以看到最终createHostRootFiber拿到的tag值其实是LegacyRoot, 所以mode最终会等于NoMode. 随后如果在开发环境下还会被赋予一个ProfileMode, 也就是我们之前说的调试模式.

所以即使我们升级到了18.x的版本, 但是如果仍然使用ReactDOM.render来创建我们的应用, 我们就会在这个条件得到false

(fiber.mode & ConcurrentMode) === NoMode

因为我们的fiber.mode并没有被赋值为ConcurrentMode.而当我们使用ReactDOM.createRoot来创建应用时, 也是一路跟着函数找下去, 会发现tag最后拿到的是ConcurrentRoot, 也就是说mode会被赋值为ConcurrentMode, 所以也就是为什么只有使用了ReactDOM.createRoot的应用才会有该特性的原因.

ps: mode是可以被赋值为多个值的, 区分这多个值是通过&操作和|操作, 因为mode其实是一个二进制值, 不理解这块的同学可以再想想二进制值的&、|操作.

!( ReactCurrentActQueue$1.isBatchingLegacy)

其实这个条件注释已经写的很清楚了

// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.

就是在act函数调用的时候也进行同步更新, act是个啥玩意呢.我们直接抄一下官网的介绍:

在编写UI测试时,可以将渲染、用户事件或数据获取等任务视为与用户界面交互的“单元”。react-dom/test-utils提供了一个名为act()的 helper, 它确保在进行任何断言之前, 与这些“单元”相关的所有更新都已处理并应用于DOM:

act(() => {// 渲染组件
});
// 进行断言

这有助于使测试运行更接近真实用户在使用应用程序时的体验。这些示例的其余部分使用act()来作出这些保证。

而isBatchingLegacy也正是在act函数中被赋值为true

function act(callback) {{// `act` calls can be nested, so we track the depth. This represents the// number of `act` scopes on the stack.var prevActScopeDepth = actScopeDepth;actScopeDepth++;if (ReactCurrentActQueue.current === null) {// This is the outermost `act` scope. Initialize the queue. The reconciler// will detect the queue and use it instead of Scheduler.ReactCurrentActQueue.current = [];}var prevIsBatchingLegacy = ReactCurrentActQueue.isBatchingLegacy;var result;try {// Used to reproduce behavior of `batchedUpdates` in legacy mode. Only// set to `true` while the given callback is executed, not for updates// triggered during an async event, because this is how the legacy// implementation of `act` behaved.ReactCurrentActQueue.isBatchingLegacy = true;result = callback(); // Replicate behavior of original `act` implementation in legacy mode,// which flushed updates immediately after the scope function exits, even// if it's an async function.if (!prevIsBatchingLegacy && ReactCurrentActQueue.didScheduleLegacyUpdate) {var queue = ReactCurrentActQueue.current;if (queue !== null) {ReactCurrentActQueue.didScheduleLegacyUpdate = false;flushActQueue(queue);}}} catch (error) {popActScope(prevActScopeDepth);throw error;} finally {ReactCurrentActQueue.isBatchingLegacy = prevIsBatchingLegacy;}//...省略}
}

综上所述

所以, 在升级到18版本之后的react只有在你使用ReactDOM.render的时候(LegacyMode)才会保持之前的行为, 否则都会对你的更新进行合并处理, 也就是自动批处理. 从我们最后调用的函数名也能看出这一点: flushSyncCallbacksOnlyInLegacyMode

被遗忘的 flushSync

我们之前还提到, 如果我们想进行同步更新可以使用flushSync函数, 那么它又干了啥.

function flushSync(fn) {// In legacy mode, we flush pending passive effects at the beginning of the// next event, not at the end of the previous one.if (rootWithPendingPassiveEffects !== null && rootWithPendingPassiveEffects.tag === LegacyRoot && (executionContext & (RenderContext | CommitContext)) === NoContext) {flushPassiveEffects();}var prevExecutionContext = executionContext;executionContext |= BatchedContext;var prevTransition = ReactCurrentBatchConfig$3.transition;var previousPriority = getCurrentUpdatePriority();try {ReactCurrentBatchConfig$3.transition = 0;setCurrentUpdatePriority(DiscreteEventPriority);if (fn) {return fn();} else {return undefined;}} finally {setCurrentUpdatePriority(previousPriority);ReactCurrentBatchConfig$3.transition = prevTransition;executionContext = prevExecutionContext; // Flush the immediate callbacks that were scheduled during this batch.// Note that this will happen even if batchedUpdates is higher up// the stack.if ((executionContext & (RenderContext | CommitContext)) === NoContext) {flushSyncCallbacks();}}
}

可以看到, 这个函数会在执行完传给他的fn函数后马上去清空一次更新队列, 也就是调用flushSyncCallbacks方法, 就是我们之前在异步中调用setState的行为.

值得一提的是, 如果我们在react 18中想达到之前的效果, 这样写是不行的:

import React from 'react';
import { flushSync } from 'react-dom';class App extends React.Component {state = {data: 1}test = () => {setTimeout(() => {flushSync(() => {this.setState({data: 2});console.log('data', this.state.data);this.setState({data: 3});console.log('data', this.state.data);});}, 0);}render() {console.log("render");return (<div><button onClick={this.test}>{this.state.data}</button></div>)}
}export default App;

这样写两个setState还是会被合并为同一个, 因为调用完setState之后并不会马上去刷新更新队列, 只有在整个函数执行完以后才会对队列进行刷新. 所以如果两个setState都写在一个flushSync里面是没有效果的.要想达到之前的效果需要这样写:

import React from 'react';
import { flushSync } from 'react-dom';class App extends React.Component {state = {data: 1}test = () => {setTimeout(() => {flushSync(() => {this.setState({data: 2});});console.log('data', this.state.data);flushSync(() => {this.setState({data: 3});});console.log('data', this.state.data);}, 0);}render() {console.log("render");return (<div><button onClick={this.test}>{this.state.data}</button></div>)}
}

ps: 我们一直说的同步异步并不是指setState本身, setState本身一直一个同步函数, 我们指的是调用完setState后react会同步的去执行后续的步骤还是会异步的去执行后续的步骤.

结语

react官方做出这个改变其实也是为了更好的性能去考虑的, 毕竟调用完setState之后同步的进行渲染有时候会导致很多没必要的开销, 特别是在进行数据请求时, 很容易写出多个同步的setState.

而随着这波更新, 可能以后这道经典的面试题也会随之消散, 毕竟往后不论在什么情况下, 默认行为都会帮你对setState进行合并更新, 不再会进行同步处理了.

http://www.lryc.cn/news/35062.html

相关文章:

  • Kafka消费者 TCP管理
  • 软考高级备考哪一个类型好些?
  • 2023 HBU 天梯赛第一次测试 题目集
  • 华为OD机试题,用 Java 解【子序列长度】问题
  • 内网环境解决SSL证书问题
  • 数据分析方法01对比分析法
  • 基于SMOKE多模式排放清单处理技术及EDGAR/MEIC清单制作与VOCs排放量核算
  • CSS流动布局-页面自适应
  • 3.Elasticsearch初步进阶
  • 优思学院|六西格玛管理的核心理念是什么?
  • 第十七节 多态
  • [vue]提供一种网站底部备案号样式代码
  • python第四天作业~函数练习
  • linux安装influxdb-rpmyum方式
  • 死锁
  • C++基础了解-05-C++常量
  • 深度学习笔记-2.自动梯度问题
  • 一文读懂倒排序索引涉及的核心概念
  • Java基础算法题
  • 「SAP ABAP」你真的了解OPEN SQL的DML语句吗 (附超详细案例讲解)
  • 数据结构3——线性表2:线性表的顺序结构
  • VMware虚拟机搭建环境通用方法
  • 2.Fully Convolutional Networks for Semantic Segmentation论文记录
  • 深度解析Spring Boot自动装配原理
  • Redis性能分析相关-channel=[id: 0xbee27bd4, L:/127.0.0.1:63156
  • Linux:环境变量
  • Codeforces Round 703 (Div. 2)(A~D)
  • Django项目5——基于tensorflow serving部署深度模型——windows版本
  • MySQL基础篇3
  • 携程 x TiDB丨应对全球业务海量数据增长,一栈式 HTAP 实现架构革新