react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?

壹 ❀ 引
在react中的setState是同步还是异步?react为什么要将其设计成异步?一文中,我们介绍了setState同步异步问题,解释了何种情况下同步与异步,异步带来了什么好处,以及react官方为何要将setState设计成异步。
但因为文章篇幅问题,我们遗留了一个与setState底层相关的问题,为什么在合成事件中使用setState会批量异步合并,而原生事件中setState又是同步呢?react是如何感知这两者的区分从而做不同处理,带着疑问文本开始。
贰 ❀ setState背后的秘密(旧版)
既然setState在合成与原生事件之间有所区分,那么在setState源码实现上一定会有所表现,这里我们摘出setState相关源码做一个简单分析。
注意,这里的源码版本为react 15,原因是我在阅读react 16源码过程中发现react在更新机制上已经有了Fiber的介入,若不了解Fiber理解起来就十分困难了:
enqueueSetState: function (inst, payload, callback) {
var fiber = get(inst);
// ....
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
所以还是先了解低版本的做法,对于后续理解Fiber也有一定帮助,先看看setState相关实现方法,这里做了部分代码裁剪:
ReactComponent.prototype.setState = function (partialState, callback) {
// 本质上调用的是enqueueSetState这个方法
this.updater.enqueueSetState(this, partialState)
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState')
}
}
当我们调用setState时,其实本质上调用的是this.updater.enqueueSetState,看到enqueue本能会想到队列,这里感觉就跟批量处理扯上关系了,OK,我们接着看enqueueSetState的实现:
enqueueSetState: function(publicInstance, partialState) {
// 根据传递的this,获取当前组件实例
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);
// 获取当前组件实例上的_pendingStateQueue
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
// 把当前要更新的状态push到数组中
queue.push(partialState);
// 再次调用enqueue更新方法
enqueueUpdate(internalInstance);
}
enqueueSetState做的事情也很简单,大致分为四步:
- 根据传递的
this(参数publicInstance)获取当前组件的实例internalInstance。 - 获取组件实例
internalInstance上的数组_pendingStateQueue,看名字就知道是等待被处理的state状态,而且假如不存在,这里也会帮其初始化成一个数组。 - 将我们这一次要更新的
state状态push到数组中。 - 调用队列更新方法
enqueueUpdate。
接着我们来看看enqueueUpdate相关实现:
var dirtyComponents = [];
function enqueueUpdate(component) {
ensureInjected();
// isBatchingUpdates决定了是否立刻更新this.state
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
这里,我们就看到大家有所耳闻的isBatchingUpdates(表示当前是否处理批量更新阶段),react会根据此字段来决定是否立刻更新状态。假设isBatchingUpdates为false,直接调用batchingStrategy.batchedUpdates做更新操作,假设为true,则将我们当前的组件实例加入dirtyComponents中,表示这个更新得再等一等。
那既然isBatchingUpdates是由batchingStrategy(批量更新策略)提供,我们接着看看它的内部实现:
var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
// 全局的isBatchingUpdates,一开始默认是false
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
// 这里是用于在修改isBatchingUpdates之前存储上次的isBatchingUpdates状态
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates
// 只要调用batchedUpdates就会将isBatchingUpdates改为true
ReactDefaultBatchingStrategy.isBatchingUpdates = true
// 如果在我们改isBatchingUpdates为true之前它就已经是true了,那说明改之前就已经处于批量更新状态中了
if (alreadyBatchingUpdates) {
// 那既然已经在更新了,就直接等待更新结束
return callback(a, b, c, d, e)
} else {
// 启动事务开始进行更新
return transaction.perform(callback, null, a, b, c, d, e)
}
},
}
OK,到这里情况就有点复杂了,我们提炼下信息以及可能的疑问。
ReactDefaultBatchingStrategy提供了全局的批量更新状态锁isBatchingUpdates,且一开始默认是false。- 假设我们调用
batchedUpdates,会将isBatchingUpdates改为true - 根据修改
isBatchingUpdates之前的锁的状态决定不同的处理,锁是false直接等待更新结束,是true那就开始走事务更新。
问题来了,虽然ReactDefaultBatchingStrategy提供了isBatchingUpdates但这个东西一开始就是false啊,我们目前唯一看到的锁的修改还是在batchedUpdates中。但很明显在上面的enqueueUpdate中就已经先一步对于batchedUpdates状态做判断了,那说明一定有其它地方也会修改batchedUpdates的状态,否则同步异步的执行就完全没区别了。
我们可以尝试推理下异步与同步的差异过程,假设是异步情况,当走到enqueueUpdate时按照我们的理解,此时isBatchingUpdates就应该是true,这样代码才能走到dirtyComponents.push(component)这一步,让状态更新等一等,因此一定在更之前有什么操作将isBatchingUpdates改为true,这也逻辑才合理。
而同步情况参考一开始的isBatchingUpdates默认值是false,逻辑也确实也能走到立刻更新batchedUpdates,但是要注意,batchedUpdates中是会将isBatchingUpdates改为true的,那你在定时器中写了两个setState,第一次因为锁的默认值是false算你立刻更新了,但锁被改成true了第二次同步更新怎么办?按照常理来说,一定有一个更新完成后重置锁的状态为false的动作,不然这就说不通了。
PS:题外话说一句,不要在同一组件中同时使用同步与异步更新this.state,由上分的分析就能感受到,这种做法极大可能造成更新的混乱与不可预期。
叁 ❀ react中的Transaction(事务)
通过上面的分析,我们已经得知用于区分是否立刻更新还是等等再更新的关键在于批量更新锁batchedUpdates,但紧接着我们脑补了同步与异步的执行情况,推测一定有某个地方会做提前修改锁的状态,以及更新完成后重置锁状态类似的操作,那么在哪做的呢?谁来负责这一块的逻辑呢?这就得聊聊react中的事务处理Transaction。
class Transaction {
reinitializeTransaction() {
// 获取wapper方法,是个数组
this.transactionWrappers = this.getTransactionWrappers();
}
// 事务的启动方法
perform(method, scope, ...param) {
this.initializeAll(0);
// 这里执行的method其实就是enqueueUpdate
var ret = method.call(scope, ...param);
this.closeAll(0);
return ret;
}
// 执行所有wapper中的init
initializeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.initialize.call(this);
}
}
// 执行所有wapper中的close
closeAll(startIndex) {
var transactionWrappers = this.transactionWrappers;
for (var i = startIndex; i < transactionWrappers.length; i++) {
var wrapper = transactionWrappers[i];
wrapper.close.call(this);
}
}
}
class ReactDefaultBatchingStrategyTransaction extends Transaction {
constructor() {
this.reinitializeTransaction()
}
// 返回wapper方法,是个数组
getTransactionWrappers() {
return [
// FLUSH_BATCHED_UPDATES
{
initialize: () => { },
// state更新完后,diff对比以及组件后续更新
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
},
// RESET_BATCHED_UPDATES
{
initialize: () => { },
close: () => {
// 重置锁
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
}
]
}
}
react的事务解释起来有点抽象,但我们可以先站在宏观的角度去理解,我们可以将Transaction理解成一个封闭的方法加工工厂,每当一个方法运输进去,都会通过wapper对方法进行加工,并为方法组装上initialize与close方法。
当调用transaction.perform启动事务处理时,你会发现在perform中的处理分为三步,第一执行所有wapper中的init;第二才是真正执行我们传递的callback(本质就是enqueueUpdate);第三步在callback跑完再执行所有wapper中的close。
而wapper其实也分为FLUSH_BATCHED_UPDATES与RESET_BATCHED_UPDATES两种类型,不同类型中的init我们先不管,但FLUSH_BATCHED_UPDATES中的close会在callback执行完成后帮助我们更新最新的state与props。
当我们看向RESET_BATCHED_UPDATES的close时,我们发现了一个熟悉的操作ReactDefaultBatchingStrategy.isBatchingUpdates = false,这里是我们第二次发现修改锁的状态。还记得前面我们对于原生定时器中多次执行setState的问题吗?第一次setState会将isBatchingUpdates改为true,但在执行完完成后RESET_BATCHED_UPDATES中的close会帮我们立刻重置锁的状态,这也就保证了定时器中第二个setState运行时,锁的状态又默认成了false,于是再次同步更新。
肆 ❀ 为什么钩子合成事件是异步?
事务介绍了一大堆,我们顺利解释了同步更新情况下setState是如何重置锁状态的,那么钩子函数执行setState得保证锁一开始就是true才行啊,这又是怎么回事呢?看下面这段代码:
// 摘自上方的batchedUpdates方法,知道里面有将锁改为true的操作就行
batchedUpdates: function (callback, a, b, c, d, e) {
// ...
ReactDefaultBatchingStrategy.isBatchingUpdates = true
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e)
} else {
return transaction.perform(callback, null, a, b, c, d, e)
}
}
_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
var componentInstance = instantiateReactComponent(nextElement);
// 调用batchedUpdates方法
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode,
componentInstance,
container,
shouldReuseMarkup,
context
);
}
_renderNewRootComponent其实看名字就知道是在组件渲染时执行的方法,也就是在组件初次渲染,这里就已经执行过一次batchedUpdates方法了,而batchedUpdates内部有将锁改为true的操作,这也就是为啥钩子函数中setState异步的问题。
同理,合成事件中的setState也是异步,那说明也应该有初始锁状态为true的行为,事实上确实如此,看下面代码:
dispatchEvent: function (topLevelType, nativeEvent) {
try {
// 调用了batchedUpdates修改锁状态
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
那么到这里,我们解释了钩子函数,合成事件以及原生事件中setState执行差异背后的原理。
伍 ❀ 总
其实本文一开始我是打算通过setState的差异性引出合成事件,从而介绍合成事件与原生事件的区别。但在整理setState的过程中,发现信息量惊人....而且更为离谱的是,react从16.8引入fiber开始,setState原理其实已经不再是上述那样了。但处于篇幅问题以及知识量,此篇仅介绍react 15同异步原理差异,那么下一篇正式介绍react中的合成事件,本文结束。
参考
react15 和 react16 在 setState 后的更新渲染解析
react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?的更多相关文章
- JS异步解决方案之概念理解-----------阻塞和非阻塞,同步和异步,并发和并行,单线程和多线程
首先记住一句话,JS是单线程的. 单线程意味着什么?单线程意味着 它不能依靠自己实现异步. JS实现的异步,往往都是靠 浏览器.Node 的机制(事件驱动.回调)实现的. 下面让我这个单身狗 以谈恋爱 ...
- 领导者/追随者(Leader/Followers)模型和半同步/半异步(half-sync/half-async)模型都是常用的客户-服务器编程模型
领导者-追随者(Leader/Followers)模型的比喻 半同步/半异步模型和领导者/追随者模型的区别: 半同步/半异步模型拥有一个显式的待处理事件队列,而领导者-追随者模型没有一个显式的队列(很 ...
- 【Java面试题】25 同步和异步有何异同,在什么情况下分别使用他们?举例说明。
如果数据将在线程间共享.例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取. 当应用程序在对象上调用了一个需要花费很长时间 ...
- java 线程之对象的同步和异步
一.多线程环境下的同步与异步 同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去. package com.jalja.org.th ...
- 如何理解javascript中的同步和异步
javascript语言是一门“单线程”的语言,不像java语言,类继承Thread再来个thread.start就可以开辟一个线程,所以,javascript就像一条流水线,仅仅是一条流水线而已,要 ...
- React学习小记--setState的同步与异步
react中,state不能直接修改,而是需要使用setState()来对state进行修改,那什么时候是同步而什么时候是异步呢? 基础代码: setCounter = (v) => { thi ...
- react的setState到底是同步还是异步
在介绍这个问题之前,我们先来看一下一个例子: state = {number:1};componentDidMount(){this.setState({number:3})console.log(t ...
- [React Router] Create a ProtectedRoute Component in React Router (setState callback to force update)
In this lesson we'll create a protected route just for logged in users. We'll combine a Route with a ...
- React生命周期和响应式原理(Fiber架构)
注意:只有类组件才有生命周期钩子函数,函数组件没有生命周期钩子函数. 生命周期 装载阶段:constructor() render() componentDidMount() 更新阶段:render( ...
- React.js入门笔记(续):用React的方式来思考
本文主要内容来自React官方文档中的"Thinking React"部分,总结算是又一篇笔记.主要介绍使用React开发组件的官方思路.代码内容经笔者改写为较熟悉的ES5语法. ...
随机推荐
- pycharm解决test_开头文件报错的问题
一.问题描述 运行时报了如下错误:
- java项目实战-mybatis-基本配置01-day22
目录 0. mysql navicate链接分享 1. mvn坐标引入 2. mysql的核心配置文件 3. 返回值类型 别名 4. 将数据的配置提取配置文件 4. log4j修改日志输出 0. my ...
- 部分MySQL的SQL信息整理
模块补丁信息查看 select su as 补丁模块, count(1) as 数量 from gsppatchlog where TO_DAYS( NOW( ) ) - TO_DAYS(deploy ...
- [转帖]拯救关键业务上线:DBA 的惊魂24小时
一个电话,打破深夜的宁静 9月20日晚上10点 刚完成外地一个重点项目为期2周的现场支持,从机场回家的路上,一阵急促的铃声惊醒了出租车上昏昏欲睡的我,多年的工作经验告诉我这么晚来电一定是出事了,接起电 ...
- [转帖]Kafka 与RocketMQ 落盘机制比较
https://www.jianshu.com/p/fd50befccfdd 引言 前几期的评测中,我们对比了Kafka和RocketMQ的吞吐量和稳定性,本期我们要引入一个新的评测标准--软件可靠性 ...
- [转帖]iptables命令详解和举例(完整版)
1.防火墙概述 防火墙,其实说白了讲,就是用于实现Linux下访问控制的功能的,它分为硬件的或者软件的防火墙两种.无论是在哪个网络中,防火墙工作的地方一定是在网络的边缘.而我们的任务就是需要去定义到底 ...
- [转帖]Linux实用技巧——find查找指定时间内修改过的文件或目录
https://cloud.tencent.com/developer/article/1694949 解决方案 例:查找出五分钟内修改过的文件 [root@mobius ~]$ find ./* - ...
- [转帖]Linux性能调优之内存负载调优的一些笔记
https://zhuanlan.zhihu.com/p/548770928 写在前面 整理一些Linux内存调优的笔记,分享给小伙伴 博文没有涉及的Demo,理论方法偏多,可以用作内存调优入门 博文 ...
- CentOS上面阿里源的设置过程
1. 移除已经有的yum仓库 #原因: 公司内部部分境外网站不能访问,会提示异常. rm -rf /etc/yum.repos.d/* 2. 使用阿里源进行处理. #主要有两个, 一个是base的一个 ...
- Bitmap、RoaringBitmap原理分析
作者:京东科技 曹留界 在人群本地化实践中我们介绍了人群ID中所有的pin的偏移量可以通过Bitmap存储,而Bitmap所占用的空间大小只与偏移量的最大值有关系.假如现在要向Bitmap内存入两个p ...