定义
这里所说的同步还是异步其实指的是调用setState后能否马上得到更新后的值,能得到最新值则为同步,不能得到最新值则为异步;
而不是指的setState这个函数是同步还是异步,单纯的说 setState函数肯定是同步的。
类组件
一道面试题目
import React from 'react';
import './App.css';
class AppClass extends React.Component {
state = {
count: 0,
};
handleClick = () => {
this.setState({ count: 1 });
console.log('count: ', this.state.count);
this.setState({ count: 2 });
console.log('count: ', this.state.count);
setTimeout(() => {
this.setState({ count: 3 });
console.log('count: ', this.state.count);
this.setState({ count: 4 });
console.log('count: ', this.state.count);
}, 0);
};
render() {
return (
<div className='App'>
<button onClick={this.handleClick}>count = {this.state.count}</button>
</div>
);
}
}
export default AppClass;
react17
-
第一个输出
count: 0,说明setState是异步执行的,所以在调用之后打印count还是初始值0。 -
第二个输出还是
count: 0,说明setState还是异步执行的。 -
第三个输出
count: 3,而且在打印语句前正是调用setState将count置为了3,这里的setState是同步执行的。 -
第四个输出
count: 4,而且前面也正是调用setState将count置为了4, 这里setState也是同步执行的。
在react 可调度范围内的setState 是异步的,会对更新开启批量更新;在react17中,react 合成事件内、生命周期函数内同步执行的setState 就是可调度范围;宏任务:setTimeout ,微任务:.then ,或直接在DOM元素上绑定的事件等都是react 可调度范围外。
分析下以上的输出:
-
handleClick函数是react的合成事件,所以其内部的setState是异步的 -
进入
handleClick函数内部,发现前两个setState是没有被setTimeout包裹的,在调度范围内,故表现为异步,所以前两次的输出都是0。 -
还有两个
setState是在setTimeout内的,不在react调度范围内,故表现为同步,所以每次setState执行后都可以立即获取到更新后的值。
react18
批处理:是指将多个状态更新合并为一个更新,从而减少组件重新渲染的次数,提高应用程序的性能,React 18 实现了自动批处理。
-
前两次的结果相同,都是0,证明这块是跟 v17 中一样的,都是异步
-
后两次结果不一样,v17中是同步更新的,所以每次setState 后都可以立即获取到更新后的值,但v18 中打印的是两个2 ,说明是异步更新的,只是这个异步更新跟setTimeout 外部的不在一个批中,setTimeout 中的批处理明显落后外部的批处理。
flushSync
自动批处理是安全的,但某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容。这种情况,可以选择 ReactDOM.flushSync() 退出批处理,立即执行调度更新。
import { flushSync } from 'react-dom';
state = {
count: 0,
};
handleClick = () => {
this.setState({ count: 1 });
console.log('count: ', this.state.count);
// 此处执行同步刷新
ReactDom.flushSync(() => {
this.setState({ count: 2 });
});
console.log('ReactDom.flushSync 后的 count: ', this.state.count);
setTimeout(() => {
this.setState({ count: 3 });
console.log('count: ', this.state.count);
this.setState({ count: 4 });
console.log('count: ', this.state.count);
}, 0);
};
可以看到,同步刷新后,立即可以获取到变化后的值。
函数组件
import { useState } from 'react';
import './App.css';
function App() {
const [count, setCount] = useState(0);
const handle = () => {
setCount(1);
console.log('count1: ', count);
setCount(2);
console.log('count2: ', count);
setTimeout(() => {
setCount(3);
console.log('count3: ', count);
setCount(4);
console.log('count4: ', count);
}, 0);
};
return (
<>
<div className='App'>
<button onClick={handle}>count is {count}</button>
</div>
</>
);
}
export default App;
react17
react18
为了能更清楚的看到 React 的渲染行为,修改下上边的代码,在每次渲染都都打印下当前的 count值,添加如下代码:
console.log("render count:", count);
useEffect(() => {
console.log('useEffect: 此时的count: ', count);
});
react17
- 在 React 17 下,render 打印了 3 次,说明组件重新渲染了 3 次。 React 17
setState时,批处理时渲染一次,在setTimeout中是同步的,渲染两次。 - 在函数组件中,由于闭包的原因,尽管
setState是同步的,并且count的值已经同步更新了,但仍只能获取到原值。
注意render count:3 是调用函数组件打印的, count3:0 还是handle函数打印的。
函数组件调用后就被销毁,它创建的fiber节点才会持续存在,函数组件只是一个函数,用来构建同步fiber节点的。
react18
- 在来看下 React 18 下,render 打印了 2 次,说明组件渲染了 2 次。 React18 有自动批处理,
setTimeout外部批处理一次,内部批处理一次,一共渲染两次。
总结
同步还是异步其实指的是调用setState后能否马上得到更新后的值,能得到最新值则为同步,不能得到最新值则为异步。
在 React 18 中,利用了浏览器的事件循环机制实现自动批处理。在当前事件循环的宏任务中,每次setState后都会创建一个update对象,并且挂载到fiber上。并且只会将第一个setState产生的performWorkOnRoot方法放在本次循环的微任务或者下一次循环的宏任务中执行,当前宏任务中的其他setState仅产生数据,最终在微任务或者下次宏任务中批量更新一次。
在react17中只有在合成事件、生命周期函数中的setState是异步的;setTimeOut, Promise.then 等都是同步的。
在react18中,开启了自动批处理,setTimeOut, Promise.then都加上了批处理,想同步更新的话,可以使用flushSync。
注意:在函数式组件中,尽管是同步更新时并且状态值已经变了,但因为闭包的原因无法获取到最新的值。
⚠️ 批量更新 和 并发更新 是两个概念。
批量更新指的是setState状态更新时是异步还是同步的。
并发更新和同步更新是相对的,指的是render阶段是否可以被中断,将线程交给浏览器,以此响应一些重要的事件。