关于React setState的实现原理(三)
前面提到事务即将结束时,会去调用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的实现原理(三)的更多相关文章
- 关于React setState的实现原理(二)
React中的Transaction 大家学过sql server的都知道我们可以批量处理sql语句,原理其实都是基于上一篇我们说的Datch Update机制.当所有的操作均执行成功,才会执行修改操 ...
- 关于React setState的实现原理(一)
前言 首先在学习react的时候就对setSate的实现有比较浓厚的兴趣,那么对于下边的代码,可以快速回答吗? class Root extends React.Component { constru ...
- 并发之AQS原理(三) 如何保证并发
并发之AQS原理(三) 如何保证并发 1. 如何保证并发 AbstractQueuedSynchronizer 维护了一个state(代表了共享资源)和一个FIFO线程等待队列(多线程竞争资源被阻塞时 ...
- React Native 入门到原理(详解)
抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...
- The Road to learn React书籍学习笔记(第三章)
The Road to learn React书籍学习笔记(第三章) 代码详情 声明周期方法 通过之前的学习,可以了解到ES6 类组件中的生命周期方法 constructor() 和 render() ...
- 深入理解React:事件机制原理
目录 序言 DOM事件流 事件捕获阶段.处于目标阶段.事件冒泡阶段 addEventListener 方法 React 事件概述 事件注册 document 上注册 回调函数存储 事件分发 小结 参考 ...
- 跟vczh看实例学编译原理——三:Tinymoe与无歧义语法分析
文章中引用的代码均来自https://github.com/vczh/tinymoe. 看了前面的三篇文章,大家应该基本对Tinymoe的代码有一个初步的感觉了.在正确分析"print ...
- word2vec原理(三) 基于Negative Sampling的模型
word2vec原理(一) CBOW与Skip-Gram模型基础 word2vec原理(二) 基于Hierarchical Softmax的模型 word2vec原理(三) 基于Negative Sa ...
- 动态修改JS对象的值及React setState
一.在JS里使用(非ES6) 实现场景: 给一个空对象填充某一指定数组内的值 并随机生成数量 const fruit = ['apple', 'banana', 'orange'] let fruit ...
随机推荐
- ftp文件上传和下载
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.N ...
- 【工具】代码生成器-python脚本
我觉得造轮子这件事情,是谁都可以做的.只不过做得好或者不好而已,用心了做得就要优雅一点. 之前用过java的代码生成器,什么pojodobodbo都能生成,于是我也来自己造一个轮子. 造轮子的事情是没 ...
- (转)SpringBoot非官方教程 | 第十二篇:springboot集成apidoc
首先声明下,apidoc是基于注释来生成文档的,它不基于任何框架,而且支持大多数编程语言,为了springboot系列的完整性,所以标了个题. 一.apidoc简介 apidoc通过在你代码的注释来生 ...
- 使用Ajax验证用户是否已存在
在服务器端使用Servlet,里面在集合里存了几个字符串,没有对数据库操作. 前台input页面和Ajax验证: <%@ page language="java" conte ...
- php生成二维码的几种方式
一些php生成二维码的方式:1.google开放api:2.php类库PHP QR Code:3.libqrencode:4.QRcode Perl CGI & PHP scripts 1.g ...
- CNN学习笔记:批标准化
CNN学习笔记:批标准化 Batch Normalization Batch Normalization, 批标准化, 是将分散的数据统一的一种做法, 也是优化神经网络的一种方法. 在神经网络的训练过 ...
- uva1351 dp
这题说的是给了 一个串 然后 比如 aaaaabbbbbbcdddd 可以化成5(a)6(b)c4(d) 这样的串明显 长度更短了 , 请 计算出使得这个串最短的 长度是多少, dp[i][j] 表示 ...
- EditPlus 4.3.2475 中文版已经发布(10月28日更新)
新的修订版修复了上移多个插入点时会造成程序崩溃的问题.
- DB开发之mysql
1. MySQL 4.x版本及以上版本提供了全文检索支持,但是表的存储引擎类型必须为MyISAM,以下是建表SQL,注意其中显式设置了存储引擎类型 CREATE TABLE articles ( id ...
- excel文件与txt文件互转,并且把excel里的数据导入到oracle中
一.excel文件转换成txt文件的步骤 a.首先要把excel文件转换成txt文件 1.Excel另存为中已经包含了TXT格式,所以我们可以直接将Excel表格另存为TXT格式,但是最后的效果好像不 ...