壹 ❀ 引

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做的事情也很简单,大致分为四步:

  1. 根据传递的this(参数publicInstance)获取当前组件的实例internalInstance
  2. 获取组件实例internalInstance上的数组_pendingStateQueue,看名字就知道是等待被处理的state状态,而且假如不存在,这里也会帮其初始化成一个数组。
  3. 将我们这一次要更新的state状态push到数组中。
  4. 调用队列更新方法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会根据此字段来决定是否立刻更新状态。假设isBatchingUpdatesfalse,直接调用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,到这里情况就有点复杂了,我们提炼下信息以及可能的疑问。

  1. ReactDefaultBatchingStrategy提供了全局的批量更新状态锁isBatchingUpdates,且一开始默认是false
  2. 假设我们调用batchedUpdates,会将isBatchingUpdates改为true
  3. 根据修改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对方法进行加工,并为方法组装上initializeclose方法。

当调用transaction.perform启动事务处理时,你会发现在perform中的处理分为三步,第一执行所有wapper中的init;第二才是真正执行我们传递的callback(本质就是enqueueUpdate);第三步在callback跑完再执行所有wapper中的close

wapper其实也分为FLUSH_BATCHED_UPDATESRESET_BATCHED_UPDATES两种类型,不同类型中的init我们先不管,但FLUSH_BATCHED_UPDATES中的close会在callback执行完成后帮助我们更新最新的stateprops

当我们看向RESET_BATCHED_UPDATESclose时,我们发现了一个熟悉的操作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的过程中,发现信息量惊人....而且更为离谱的是,react16.8引入fiber开始,setState原理其实已经不再是上述那样了。但处于篇幅问题以及知识量,此篇仅介绍react 15同异步原理差异,那么下一篇正式介绍react中的合成事件,本文结束。

参考

深入 React 的 setState 机制

React 架构的演变 - 从同步到异步

react15 和 react16 在 setState 后的更新渲染解析

react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?的更多相关文章

  1. JS异步解决方案之概念理解-----------阻塞和非阻塞,同步和异步,并发和并行,单线程和多线程

    首先记住一句话,JS是单线程的. 单线程意味着什么?单线程意味着 它不能依靠自己实现异步. JS实现的异步,往往都是靠 浏览器.Node 的机制(事件驱动.回调)实现的. 下面让我这个单身狗 以谈恋爱 ...

  2. 领导者/追随者(Leader/Followers)模型和半同步/半异步(half-sync/half-async)模型都是常用的客户-服务器编程模型

    领导者-追随者(Leader/Followers)模型的比喻 半同步/半异步模型和领导者/追随者模型的区别: 半同步/半异步模型拥有一个显式的待处理事件队列,而领导者-追随者模型没有一个显式的队列(很 ...

  3. 【Java面试题】25 同步和异步有何异同,在什么情况下分别使用他们?举例说明。

    如果数据将在线程间共享.例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取. 当应用程序在对象上调用了一个需要花费很长时间 ...

  4. java 线程之对象的同步和异步

    一.多线程环境下的同步与异步 同步:A线程要请求某个资源,但是此资源正在被B线程使用中,因为同步机制存在,A线程请求不到,怎么办,A线程只能等待下去. package com.jalja.org.th ...

  5. 如何理解javascript中的同步和异步

    javascript语言是一门“单线程”的语言,不像java语言,类继承Thread再来个thread.start就可以开辟一个线程,所以,javascript就像一条流水线,仅仅是一条流水线而已,要 ...

  6. React学习小记--setState的同步与异步

    react中,state不能直接修改,而是需要使用setState()来对state进行修改,那什么时候是同步而什么时候是异步呢? 基础代码: setCounter = (v) => { thi ...

  7. react的setState到底是同步还是异步

    在介绍这个问题之前,我们先来看一下一个例子: state = {number:1};componentDidMount(){this.setState({number:3})console.log(t ...

  8. [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 ...

  9. React生命周期和响应式原理(Fiber架构)

    注意:只有类组件才有生命周期钩子函数,函数组件没有生命周期钩子函数. 生命周期 装载阶段:constructor() render() componentDidMount() 更新阶段:render( ...

  10. React.js入门笔记(续):用React的方式来思考

    本文主要内容来自React官方文档中的"Thinking React"部分,总结算是又一篇笔记.主要介绍使用React开发组件的官方思路.代码内容经笔者改写为较熟悉的ES5语法. ...

随机推荐

  1. java基础-流程控制-day04

    目录 1. if单分支 2. if else 多分支 3. if else双分支 4. 随机生成一定区间的整数 5 switch语句 6. while循环 7. for循环 8. break cont ...

  2. ACP 知识点总结

    记录下学习ACP过程不断遇到的且需要记录的知识点: 在阿里云专有网络VPC创建之后,路由器也是随着VPC一起自动创建,所以不需要手动创建,这个时候需要继续创建交换机才能在交换机种创建其他云产品. 7层 ...

  3. 26-IP调用 - PLL

    1.PLL IP核简介 PLL(Phaze Locked Loop)锁相环是最常用的IP核之一,其性能强大,可以对输入到FPGA的时钟信号进行任意的分频.倍频.相位调整.占空比调整,从而输出一个期望时 ...

  4. 使用VS开发人员工具观察类在内存中的布局

    1.先要生成相应文件 2.打开VS2019开发人员工具 3.cd至文件目录 4.输入cl /d1 reportSingleClassLayoutanimal demo.cpp 其中reportSing ...

  5. CoreDNS -- DNS服务与服务发现

    CoreDNS -- DNS服务与服务发现 DNS服务器 手册:https://coredns.io/manual/toc/ Github:https://github.com/coredns/cor ...

  6. 【面试题精讲】你知道MySQL中有哪些隔离级别吗

    有时博客内容会有变动,首发博客是最新的,其他博客地址可能未同步,请认准https://blog.zysicyj.top 首发博客地址 系列文章地址 脏读(Dirty Read)是指一个事务读取到了另一 ...

  7. [转帖]Linux字符截取命令-cut

    概述 cut是一个选取命令,.一般来说,选取信息通常是针对"行"来进行分析的,并不是整篇信息分析的. 语法 cut [-bn] [file] 1 或 cut [-c] [file] ...

  8. [转帖]使用 TiUP 升级 TiDB

    本文档适用于以下升级路径: 使用 TiUP 从 TiDB 4.0 版本升级至 TiDB 7.1. 使用 TiUP 从 TiDB 5.0-5.4 版本升级至 TiDB 7.1. 使用 TiUP 从 Ti ...

  9. 内网CentOS7搭建ntp服务器实现内网时间同步

    内网CentOS7搭建ntp服务器实现内网时间同步 背景 公司内部有很多虚拟机,本来很简单的实现了每天晚上自动同步阿里云时间 crontab -e 1 1 * * * ntpdate ntp.aliy ...

  10. Vue3中hook的简单使用

    创建文件夹 在src下创建文件夹.文件名称为hooks. hooks下的文件夹下,是你的封装的hook: 通过命名为useXXXXXX usexy.js 文件是封装的获取屏幕的坐标 import { ...