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语法. ...
随机推荐
- 每天学五分钟 Liunx 0000 | 存储篇:GlusterFS
GlusterFS GlusterFS,是一个分布式文件系统,它通过 TCP/IP 或 IB(InfiniBand RDMA)网络将多个存储资源整合在一起,提供存储服务,具有很强的可扩展能力. G ...
- spring启动流程 (4) FactoryBean详解
FactoryBean接口 实现类对象将被用作创建Bean实例的工厂,即调用getObject()方法返回的对象才是真正要使用的Bean实例,而不是直接将FactoryBean对象作为暴露的Bean实 ...
- 【ThreadX-NetX Duo】Azure RTOS NetX Duo概述
Azure RTOS NetX Duo嵌入式TCP / IP网络堆栈是Microsoft高级的工业级双IPv4和IPv6 TCP / IP网络堆栈,专门为深度嵌入式,实时和IoT应用程序设计.NetX ...
- mysql-字符函数-拼接-长度-切片-替换
- 百度网盘(百度云)SVIP超级会员共享账号每日更新(2024.01.08)
一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...
- Oracle12c On 银河麒麟v10SP3 的安装过程
Oracle12c On 银河麒麟的安装过程 学习官网资料 下载最新版的preinstall文件 https://yum.oracle.com/repo/OracleLinux/OL8/appstre ...
- Python学习之八_调用Outlook发送邮件以及调用远程windows上面的python
Python学习之八_调用Outlook发送邮件以及调用远程windows上面的python 摘要 之前只有一个需求是发送加密邮件. 之前一直是使用linux进行发送.但是总是无法发送加密邮件. 最近 ...
- 拼多多的U盘都是垃圾
春节期间陪孩子玩拼多多的果园, 然后发现推送的U盘很便宜就买了下 结果发现.. U盘都是扩容盘,非常垃圾 如下图: 警告: 只有 255933 MB可测试,总容量 255935 MB. 媒体很可能已损 ...
- CentOS7 安装Oracle11g的过程.
1. 安装preinstall https://www.cnblogs.com/mjiu/ 里面有一个简单方法: cd /etc/yum.repos.d wget http://yum.oracle. ...
- Docker 部署 Ceph的简单方法
https://zhuanlan.zhihu.com/p/390377674 学习一下. docker部署 部署的思路和网络架构和前面分布式是一样的,区别在于命令的形式. 在每个节点安装 docker ...