前面提到事务即将结束时,会去调用FLUSH_BATCHED_UPDATES的flushBatchedUpdates方法执行批量更新,该方法会去遍历dirtyComponents,对每一项执行performUpdateIfNecessary方法,该方法代码如下:

performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
}

在我们的setState更新中,其实只会用到第二个 this._pendingStateQueue !== null 的判断,即如果_pendingStateQueue中还存在未处理的state,那就会执行updateComponent完成更新。那_pendingStateQueue是何时被处理的呢,继续看!

通过翻阅updateComponent方法,我们可以知道_pendingStateQueue是在该方法中由_processPendingState(nextProps, nextContext)方法做了一些处理,该方法传入两个参数,新的props属性和新的上下文环境,这个上下文环境可以先不用管。我们看看_processPendingState的具体实现:

_processPendingState: function (props, context) {
var inst = this._instance; // _instance保存了Constructor的实例,即通过ReactClass创建的组件的实例
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null; if (!queue) {
return inst.state;
} if (replace && queue.length === 1) {
return queue[0];
} var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
} return nextState;
},

什么replace啊什么的都可以暂时不用看,主要先看for循环内部做的事情,replace我们暂时认为是false。for循环遍历了_pendingStateQueue中所有保存的状态,对于每一个状态进行处理,处理时首先判断保存的是function还是object。若是function,就在inst的上下文中执行该匿名函数,该函数返回一个代表新state的object,然后执行assign将其与原有的state合并;若是object,则直接与state合并。注意,传入setState的第一个参数如果是function类型,我们可以看到,其第一个参数nextState即表示更新之前的状态;第二个参数props代表更新之后的props,第三个context代表新的上下文环境。之后返回合并后的state。这里还需要注意一点,这一点很关键,代码中出现了this._pendingStateQueue = null这么一段,这也就意味着dirtyComponents进入下一次循环时,执行performUpdateIfNecessary不会再去更新组件,这就实现了批量更新,即只做一次更新操作,React在更新组件时就是用这种方式做了优化。

好了,回来看我们的案例,当我们传入函数作为setState的第一个参数时,我们用该函数提供给我们的state参数来访问组件的state。该state在代码中就对应nextState这个值,这个值在每一次for循环执行时都会对其进行合并,因此第二次执行setState,我们在函数中访问的state就是第一次执行setState后已经合并过的值,所以会打印出2。然而直接通过this.state.count来访问,因为在执行对_pendingStateQueue的for循环时,组件的update还未执行完,this.state还未被赋予新的值,其实了解一下updateComponent会发现,this.state的更新会在_processPendingState执行完执行。所以两次setState取到的都是this.state.count最初的值0,这就解释了之前的现象。其实,这也是React为了解决这种前后state依赖但是state又没及时更新的一种方案,因此在使用时大家要根据实际情况来判断该用哪种方式传参。

接下来我们再来看看setState的第二个参数,回调函数,它是在什么时候执行的。

class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
} componentDidMount() {
let me = this;
setTimeout(function() {
me.setState({count: me.state.count + 1}, function() {
console.log('did callback');
});
console.log('hello');
}, 0);
} componentDidUpdate() {
console.log('did update');
} render() {
return <h1>{this.state.count}</h1>
}
}

这个案例控制台打印顺序是怎样的呢?不卖关子了,答案是did update,did callback,hello。这里是在一个setTimeout中执行了setState,因此其处于一个单独的事务之中,所以hello最后打印容易理解。然后我们来看看setState执行更新时做了些啥。前面我们知道在执行完组件装载即调用了componentDidMount之后,事务开始执行一系列close方法,这其中包括调用FLUSH_BATCHED_UPDATES中的flushBatchedUpdates,我们来看看这段代码。

var flushBatchedUpdates = function () {
// ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
// array and perform any updates enqueued by mount-ready handlers (i.e.,
// componentDidUpdate) but we need to check here too in order to catch
// updates enqueued by setState callbacks and asap calls.
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction); // 处理批量更新
ReactUpdatesFlushTransaction.release(transaction);
} if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll(); // 处理callback
CallbackQueue.release(queue);
}
}
};

可以看我做了中文标注的两个地方,这个方法其实主要就是处理了组件的更新和callback的调用。组件的更新发生在runBatchedUpdates这个方法中,下面的queue.notifyAll内部其实就是从队列中去除callback调用,因此应该是先执行完更新,调用componentDidUpdate方法之后,再去执行callback,就有了我们上面的结果。

总结

React在组件更新方面做了很多优化,这其中就包括了上述的批量更新。在componentDidMount中执行了N个setState,如果执行N次更新是件很傻的事情。React利用其独特的事务实现,做了这些优化。正是因为这些优化,才造成了上面见到的怪现象。还有一点,再使用this.state时一定要注意组件的生命周期,很多时候在获取state的时候,组件更新还未完成,this.state还未改变,这是很容易造成bug的一个地方,要避免这个问题,需要对组件生命周期有一定的了解。

关于React setState的实现原理(三)的更多相关文章

  1. 关于React setState的实现原理(二)

    React中的Transaction 大家学过sql server的都知道我们可以批量处理sql语句,原理其实都是基于上一篇我们说的Datch Update机制.当所有的操作均执行成功,才会执行修改操 ...

  2. 关于React setState的实现原理(一)

    前言 首先在学习react的时候就对setSate的实现有比较浓厚的兴趣,那么对于下边的代码,可以快速回答吗? class Root extends React.Component { constru ...

  3. 并发之AQS原理(三) 如何保证并发

    并发之AQS原理(三) 如何保证并发 1. 如何保证并发 AbstractQueuedSynchronizer 维护了一个state(代表了共享资源)和一个FIFO线程等待队列(多线程竞争资源被阻塞时 ...

  4. React Native 入门到原理(详解)

    抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...

  5. The Road to learn React书籍学习笔记(第三章)

    The Road to learn React书籍学习笔记(第三章) 代码详情 声明周期方法 通过之前的学习,可以了解到ES6 类组件中的生命周期方法 constructor() 和 render() ...

  6. 深入理解React:事件机制原理

    目录 序言 DOM事件流 事件捕获阶段.处于目标阶段.事件冒泡阶段 addEventListener 方法 React 事件概述 事件注册 document 上注册 回调函数存储 事件分发 小结 参考 ...

  7. 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析

    文章中引用的代码均来自https://github.com/vczh/tinymoe.   看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...

  8. word2vec原理(三) 基于Negative Sampling的模型

    word2vec原理(一) CBOW与Skip-Gram模型基础 word2vec原理(二) 基于Hierarchical Softmax的模型 word2vec原理(三) 基于Negative Sa ...

  9. 动态修改JS对象的值及React setState

    一.在JS里使用(非ES6) 实现场景: 给一个空对象填充某一指定数组内的值 并随机生成数量 const fruit = ['apple', 'banana', 'orange'] let fruit ...

随机推荐

  1. EdrawSoft Edraw Max 9.1安装破解

    1,安装软件[不要运行软件] 2,断网 3,打开Crack文件夹,复制”BaseCore.dll”,”ObjectModule.dll”到软件安装目录下替换原文件 默认的安装路径C:\Program ...

  2. [转载]WorldWind实时确定、更新、初始化和渲染地形和纹理数据

    WorldWind实时确定.更新.初始化和渲染地形和纹理数据 原文链接: http://www.cnblogs.com/rainbow70626/p/5597267.html 当用户点击WorldWi ...

  3. python之轮询、长轮询、websocket

    轮询 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息. 1.后端代码 from flask import Flask,render_templat ...

  4. Keras实践:模型可视化

    Keras实践:模型可视化 安装Graphviz 官方网址为:http://www.graphviz.org/.我使用的是mac系统,所以我分享一下我使用时遇到的坑. Mac安装时在终端中执行: br ...

  5. 39XML文档类

    Xml源代码 domxml.h #ifndef DOMXML_H #define DOMXML_H #include <QString> #include <QStringList& ...

  6. 在Qt中如何编写插件,加载插件和卸载插件(转)

    Qt提供了一个类QPluginLoader来加载静态库和动态库,在Qt中,Qt把动态库和静态库都看成是一个插件,使用QPluginLoader来加载和卸载这些库.由于在开发项目的过程中,要开发一套插件 ...

  7. 《Java入门第二季》第五章 阶段练习

    /** * ┏┓ ┏┓ * ┏┛┻━━━┛┻┓ * ┃ ┃ * ┃ ━ ┃ * ┃ > < ┃ * ┃ ┃ * ┃... ⌒ ... ┃ * ┃ ┃ * ┗━┓ ┏━┛ * ┃ ┃ Cod ...

  8. [置顶] SNMPv3认证和加密过程

    前面的一些文章详细讲解了SNMPv3的报文内容,下面主要的内容就是SNMPv3的加密和认证过程! USM的定义为实现以下功能: 鉴别 数据加密 密钥管理 时钟同步化 避免延时和重播攻击 1.UsmSe ...

  9. Ubuntu Budgie 18.04 是最好的Remix【转】

    本文转载子:https://www.linuxidc.com/Linux/2018-05/152223.htm [日期:2018-05-05] 来源:Linux公社  作者:醉落红尘 [字体:大 中  ...

  10. 一个简单的Python字符串处理文件

    #!/usr/bin/env python3 import re def lineprocess(line): res = '' index = 47 if line[index] == 'e': i ...