深入剖析setState同步异步机制
关于 setState
setState 的更新是同步还是异步,一直是人们津津乐道的话题。不过,实际上如果我们需要用到更新后的状态值,并不需要强依赖其同步/异步更新机制。在类组件中,我们可以通过this.setState
的第二参数、componentDidMount
、componentDidUpdate
等手段来取得更新后的值;而在函数式组件中,则可以通过useEffect
来获取更新后的状态。所以这个问题,其实有点无聊。
不过,既然大家都这么乐于讨论,今天我们就系统地梳理一下这个问题,主要分为两方面来说:
- 类组件(
class-component
)的更新机制 - 函数式组件(
function-component
)的更新机制
类组件中的 this.setState
在类组件中,这个问题的答案是多样的,首先抛第一个结论:
- 在
legacy
模式中,更新可能为同步,也可能为异步; - 在
concurrent
模式中,一定是异步。
问题一、legacy 模式和 concurrent 模式是什么鬼?
通过
ReactDOM.render(<App />, rootNode)
方式创建应用,则为 legacy 模式,这也是create-react-app
目前采用的默认模式;通过
ReactDOM.unstable_createRoot(rootNode).render(<App />)
方式创建的应用,则为concurrent模式
,这个模式目前只是一个实验阶段的产物,还不成熟。
legacy 模式下可能同步,也可能异步?
是的,这不是玄学,我们来先抛出结论,再来逐步解释它。
- 当直接调用时
this.setState
时,为异步更新; - 当在异步函数的回调中调用
this.setState
,则为同步更新; - 当放在自定义 DOM 事件的处理函数中时,也是同步更新。
实验代码如下:
class StateDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
}
increase = () => {
this.setState({
count: this.state.count + 1
})
// 异步的,拿不到最新值
console.log('count', this.state.count)
// setTimeout 中 setState 是同步的
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
// 同步的,可以拿到
console.log('count in setTimeout', this.state.count)
}, 0)
}
bodyClickHandler = () => {
this.setState({
count: this.state.count + 1
})
// 可以取到最新值
console.log('count in body event', this.state.count)
}
componentDidMount() {
// 自己定义的 DOM 事件,setState 是同步的
document.body.addEventListener('click', this.bodyClickHandler)
}
componentWillUnmount() {
// 及时销毁自定义 DOM 事件
document.body.removeEventListener('click', this.bodyClickHandler)
}
}
要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。
首先我们来看看 setState 的主流程:
- 调用
this.setState(newState)
; newState
会存入 pending 队列; 3,判断是不是batchUpdate
; 4,如果是batchUpdate
,则将组件先保存在所谓的脏组件dirtyComponents
中;如果不是batchUpdate
,那么就遍历所有的脏组件,并更新它们。
由此我们可以判定:所谓的异步更新,都命中了batchUpdate
,先保存在脏组件中就完事;而同步更新,总是会去更新所有的脏组件。
非常有意思,看来是否命中batchUpdate
是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate
,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?
这就涉及到一个很有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些我们看不到的东西,现在我们通过魔法让其显现出来:
increase = () => {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count)
// 结束
// isBatchingUpdates = false
}
increase = () => {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
setTimeout(() => {
// 此时isBatchingUpdates已经设置为了false
this.setState({
count: this.state.count + 1
})
console.log('count in setTimeout', this.state.count)
}, 0)
// 结束
// isBatchingUpdates = false
}
当 react 执行我们所书写的函数时,会默认在首位设置isBatchingUpdates
变量。看到其中的差异了吗?当 setTimeout 执行其回调时,isBatchingUpdates
早已经在同步代码的末尾被置为false
了,所以没命中batchUpdate
。
那自定义 DOM 事件又是怎么回事?代码依然如下:
componentDidMount() {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
document.body.addEventListener("click", () => {
// 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了
this.setState({
count: this.state.count + 1,
});
console.log("count in body event", this.state.count); // 可以取到最新值。
});
// 结束
// isBatchingUpdates = false
}
我们可以看到,当componentDidMount
跑完时,isBatchingUpdates
已经设置为false
了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates
当然也是false
,不会命中batchUpdate
机制。
总结:
this.setState
是同步还是异步,关键就是看能否命中batchUpdate
机制- 能不能命中,就是看
isBatchingUpdates
是true
还是false
- 能命中
batchUpdate
的场景包括:生命周期和其调用函数、React中注册的事件和其调用函数。总之,是React可以“管理”的入口,关键是“入口”。
这里要注意一点:React去加isBatchingUpdate的行为不是针对“函数”,而是针对“入口”。比如setTimeout、setInterval、自定义DOM事件的回调等,这些都是React“管不到”的入口,所以不会去其首尾设置isBatchingUpdates变量。
concurrent 模式一定是异步更新
因为这个东西只在实验阶段,所以要开启 concurrent 模式,同样需要将 react 升级为实验版本,安装如下依赖:
npm install react@experimental react-dom@experimental
其他代码不用变,只更改 index 文件如下:
- ReactDOM.render(<App />, document.getElementById('root'));
+ ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);
则可以发现:其更新都是同步的,在任何情况下都是如此。
关于函数式组件中 useState 的 setter
在函数式组件中,我们会这样定义状态:
const [count, setCount] = useState(0)
这时候,我们发现当我们无论在同步函数还是在异步回调中调用 setCount 时,打印出来的 count 都是旧值,这时候我们会说:setCount 是异步的。
const [count, setCount] = useState(0);
// 直接调用
const handleStrightUpdate = () => {
setCount(1);
console.log(count); // 0
};
// 放在setTimeout回调中
const handleSetTimeoutUpdate = () => {
setTimeout(() => {
setCount(1);
console.log(count); // 0
});
};
setCount 是异步的,这确实没错,但是产生上述现象的原因不只是异步更新这么简单。原因主要有以下两点:
1,调用 setCount 时,会做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders
),从这个角度上来说,setCount
确实是一个异步操作;
2,函数式的capture-value
特性决定了console.log(count)
语句打印的始终是一个只存在于当前帧的常量,所以就算无论 setCount 是不是同步的,这里都会打印出旧值。
深入剖析setState同步异步机制的更多相关文章
- setState同步异步场景
setState同步异步场景 React通过this.state来访问state,通过this.setState()方法来更新state,当this.setState()方法被调用的时候,React会 ...
- Python并发编程系列之常用概念剖析:并行 串行 并发 同步 异步 阻塞 非阻塞 进程 线程 协程
1 引言 并发.并行.串行.同步.异步.阻塞.非阻塞.进程.线程.协程是并发编程中的常见概念,相似却也有却不尽相同,令人头痛,这一篇博文中我们来区分一下这些概念. 2 并发与并行 在解释并发与并行之前 ...
- js的线程和同步异步以及console.log机制
项目上线了,闲下来就写写东西吧.积累了好多东西都没有做笔记~挑几个印象深刻的记录一下吧. js的同步异步以及单线程问题: 都知道单线程是js的一大特性.但是通常io(ajax获取服务器数据).用户/浏 ...
- 同步异步,阻塞非阻塞 和nginx的IO模型
同步与异步 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication).所谓同步,就是在发出一个*调用*时,在没有得 ...
- 进程&线程 同步异步&阻塞非阻塞
2015-08-19 15:23:38 周三 线程 线程安全 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码 线程安全问题都是由全局变量及静态变量引起的 若每个线程中对 ...
- I/O阻塞非阻塞,同步异步
http://www.cnblogs.com/luotianshuai/p/5098408.html "阻塞"与"非阻塞"与"同步"与&qu ...
- 【转载】高性能IO设计 & Java NIO & 同步/异步 阻塞/非阻塞 Reactor/Proactor
开始准备看Java NIO的,这篇文章:http://xly1981.iteye.com/blog/1735862 里面提到了这篇文章 http://xmuzyq.iteye.com/blog/783 ...
- JavaScript的异步机制
我们经常说JS是单线程的,比如node.js研讨会上大家都说JS的特色之一是单线程的,这样使JS更简单明了,可是大家真的理解所谓JS的单线程机制吗?单线程时,基于事件的异步机制又该当如何 1 先看下两 ...
- JavaScript单线程和异步机制
随着对JavaScript学习的深入和实践经验的积累,一些原理和底层的东西也开始逐渐了解.早先也看过一些关于js单线程和事件循环的文章,不过当时看的似懂非懂,只留了一个大概的印象:浏览器中的js程序时 ...
随机推荐
- 七、Jmeter测试元件-线程
线程组元素是任何测试计划的起点.所有控制器和采样器必须在线程组下.其他元素(例如,侦听器)可以直接放置在测试计划下. 名称:相当于一个业务流程 继续:当取样器出错时 会继续请求: 举例:1-当一个线程 ...
- oracle 11.2.0.4下载
Oracle 11.2.0.4下载地址 Linux x86: https://updates.oracle.com/Orion/Services/download/p13390677_112040_L ...
- Elasticsearch.Net
今天使用Elasticsearch作开发,很简单的查询,就出现Elasticsearch.Net.UnexpectedElasticsearchClientException异常,看样子像是序列化的异 ...
- 《Stereo R-CNN based 3D Object Detection for Autonomous Driving》论文解读
论文链接:https://arxiv.org/pdf/1902.09738v2.pdf 这两个月忙着做实验 博客都有些荒废了,写篇用于3D检测的论文解读吧,有理解错误的地方,烦请有心人指正). 博客原 ...
- learn Docker from scratch (1)
一.前言 Docker容器一个很有趣的东西,下面链接内容适合docker的入门非常棒! 链接如下: http://www.ruanyifeng.com/blog/2018/02/docker-tuto ...
- Intellij idea 创建java web
https://blog.csdn.net/qq_41592771/article/details/80577791 将tomcat服务器lib目录下面的servlet-api.jar复制到lib文件 ...
- 移动端 Swiper
一.什么是swiper 开源.免费.强大的触摸滑动插件 Swiper常用于移动端网站的内容触摸滑动 Swiper能实现触屏焦点图.触屏Tab切换.触屏多图切换等常用效果 #二.如何使用 1.首先加载插 ...
- 使用vika维格表来管理寺庙原来如此轻松~
我有一款适合用于寺庙管理的软件推荐,它是vika维格表,一款一站式的项目管理工具. 一站式项目管理 一个小小的寺庙需要管理的内容也非常的多,你应该不会购买多个系统去管理不同的项目,这样会让寺庙的花费大 ...
- [OI笔记]杂题整理1(基础篇~)
算是开学第四周啦,之前的三周大概过了一遍基础图论和数学相关的内容.这篇随笔打算口胡一些近期做感觉比较好的数学相关的题目 因为这段时间主要是看紫书学的,所以其实会有些出自UVA的例题,如果需要题目但是觉 ...
- [OI笔记]基础图论/图算法
[2017.8.29 00:00]--前几天开始好好学了几天的图论,不过这最近又突然因为一些原因(其实是晚上没睡好导致白天没精神)颓废了几天-一方面为了控制自己同时也可以当做之后noip前复习用的笔记 ...