深入研究React setState的工作机制
前言
上个月发表了一篇 React源码学习——ReactClass,但是后来我发现,大家对这种大量贴代码分析源码的形式并不感冒。讲道理,我自己看着也烦,还不如自己直接去翻源码来得痛快。吸取了上一次的教训,这次我决定:理性贴代码!翻阅源代码的工作还是留给各位小伙伴自己去做比较好。本来这次想准备说一说我们平时一直提到的React Virture DOM,但这可能又会造成无限贴源码的后果,因为virture dom在React中主要就是一个对象,在ReactElement中定义的,感兴趣的同学去源码中搜索一下createElement方法,就能看到virture dom是啥东西了。对其本身是没啥好说的,需要分析的应该是其在组件挂载和更新时的应用,因此对于ReactElement本身就不单独拿出来讲了,大家感兴趣就去翻阅一下源码吧。
进入正题
这次主要是要分析一下React中常见的setState方法,熟悉React的小伙伴应该都知道,该方法通常用于改变组件状态并用新的state去更新组件。但是,这个方法在很多地方的表现总是与我们的预期不符,先来看几个案例。
案例一
class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let me = this;
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印出0
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印出0
setTimeout(function(){
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印出2
}, 0);
setTimeout(function(){
me.setState({
count: me.state.count + 1
});
console.log(me.state.count); // 打印出3
}, 0);
}
render() {
return (
<h1>{this.state.count}</h1>
)
}
}
这个案例大家可能在别的地方中也见到过,结果确实让人匪夷所思,打印出0,0,2,3。先抛出两个问题:
- 为什么不在setTimeout中执行的两次setState均打印出0?
- 为什么setTimeout中执行的两次setState会打印出不同结果?
带着两个问题往下看。
React中的transaction(事务)
说到事务,我第一反应就是在以前使用sql server时用来处理批量操作的一个机制。当所有操作均执行成功,即可以commit transaction;若有一个操作失败,则执行rollback。在React中,也实现了一种类似的事务机制,其他文章也有详细的介绍。按照我个人的理解,React中一个事务其实就是按顺序调用一系列函数。在React中就是调用perform方法进入一个事务,该方法中会传入一个method参数。执行perform时先执行initializeAll方法按顺序执行一系列initialize的操作,例如一些初始化操作等等,然后执行传入的method,method执行完后就执行closeAll方法按顺序执行一系列close操作,例如下面会提到的执行批量更新或者将isBatchingUpdates变回false等等,然后结束这次事务。React中内置了很多种事务,注意,同一种事务不能同时开启,否则会抛出异常。我们还是回到我们上面的案例中来说明这个过程。
组件在调用ReactDOM.render()之后,会执行一个_renderNewRootComponent方法,大家可以去翻阅源码看一看,该方法执行了一个ReactUpdates.batchedUpdates()。batchedUpdates是什么呢?我们看看它的代码。
var transaction = new ReactDefaultBatchingStrategyTransaction(); var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false, /**
* Call the provided function in a context within which calls to `setState`
* and friends are batched such that components aren't updated unnecessarily.
*/
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates; ReactDefaultBatchingStrategy.isBatchingUpdates = true; // The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e);
}
}
};
从代码中我们可以看出,这个batchedUpdates由于是第一次被调用,alreadyBatchingUpdates为false,因此会去执行transaction.perform(method),这就将进入一个事务,这个事务具体做了啥我们暂时不用管,我们只需要知道这个transaction是ReactDefaultBatchingStrategyTransaction的实例,它代表了其中一类事务的执行。然后会在该事务中调用perform中传入的method方法,即开启了组件的首次装载。当装载完毕会调用componentDidMount(注意,此时还是在执行method方法,事务还没结束,事务只有在执行完method后执行一系列close才会结束),在该方法中,我们调用了setState,出现了一系列奇怪的现象。因此,我们再来看看setState方法,这里只贴部分代码。
ReactComponent.prototype.setState = function (partialState, callback) {
!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null) ? "development" !== 'production' ? invariant(false, 'setState(...): takes an object of state variables to update or a function which returns an object of state variables.') : _prodInvariant('85') : void 0;
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
setState在调用时做了两件事,第一,调用enqueueSetState。该方法将我们传入的partialState添加到一个叫做_pendingStateQueue的队列中去存起来,然后执行一个enqueueUpdate方法。第二,如果存在callback就调用enqueueCallback将其存入一个_pendingCallbacks队列中存起来。然后我们来看enqueueUpdate方法。
function enqueueUpdate(component) {
ensureInjected(); // Various parts of our code (such as ReactCompositeComponent's
// _renderValidatedComponent) assume that calls to render aren't nested;
// verify that that's the case. (This is called by each top-level update
// function, like setState, forceUpdate, etc.; creation and
// destruction of top-level components is guarded in ReactMount.) if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
} dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
是否看到了某些熟悉的字眼,如isBatchingUpdates和batchedUpdates。不错,其实翻阅代码就能明白,这个batchingStrategy就是上面的ReactDefaultBatchingStrategy,只是它通过inject的形式对其进行赋值,比较隐蔽。因此,我们当前的setState已经处于了这一类事务之中,isBatchingUpdates已经被置为true,所以将会把它添加到dirtyComponents中,在某一时刻做批量更新。因此在前两个setState中,并没有做任何状态更新,以及组件更新的事,而仅仅是将新的state和该组件存在了队列之中,因此两次都会打印出0,我们之前的第一个问题就解决了,还有一个问题,我们接着往下走。
在setTimeout中执行的setState打印出了2和3,有了前面的铺垫,我们大概就能得出结论,这应该就是因为这两次setState分别执行了一次完整的事务,导致state被直接更新而造成的结果。那么问题来了,为什么setTimeout中的setState会分别执行两次不同的事务?之前执行ReactDOM.render开启的事务在什么时候结束了?我们来看下列代码。
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function () {
ReactDefaultBatchingStrategy.isBatchingUpdates = false;
}
}; var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
}; var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]; function ReactDefaultBatchingStrategyTransaction() {
this.reinitializeTransaction();
} _assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
getTransactionWrappers: function () {
return TRANSACTION_WRAPPERS;
}
});
这段代码也是写在ReactDefaultBatchingStrategy这个对象中的。我们之前提到这个事务中transaction是ReactDefaultBatchingStrategyTransaction的实例,这段代码其实就是给该事务添加了两个在事务结束时会被调用的close方法。即在perform中的method执行完毕后,会按照这里数组的顺序[FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]依次调用其close方法。FLUSH_BATCHED_UPDATES是执行批量更新操作。RESET_BATCHED_UPDATES我们可以看到将isBatchingUpdates变回false,即意味着事务结束。接下来再调用setState时,enqueueUpdate不会再将其添加到dirtyComponents中,而是执行batchingStrategy.batchedUpdates(enqueueUpdate, component)开启一个新事务。但是需要注意,这里传入的参数是enqueueUpdate,即perform中执行的method为enqueueUpdate,而再次调用该enqueueUpdate方法会去执行dirtyComponents那一步。这就可以理解为,处于单独事务的setState也是通过将组件添加到dirtyComponents来完成更新的,只不过这里是在enqueueUpdate执行完毕后立即执行相应的close方法完成更新,而前面两个setState需在整个组件装载完成之后,即在componentDidMount执行完毕后才会去调用close完成更新。总结一下4个setState执行的过程就是:先执行两次console.log,然后执行批量更新,再执行setState直接更新,执行console.log,最后再执行setState直接更新,再执行console.log,所以就会得出0,0,2,3。
案例二
如下两种相似的写法,得出不同的结果。
class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let me = this;
me.setState({
count: me.state.count + 1
});
me.setState({
count: me.state.count + 1
});
}
render() {
return (
<h1>{this.state.count}</h1> //页面中将打印出1
)
}
}
class Root extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
let me = this;
me.setState(function(state, props) {
return {
count: state.count + 1
}
});
me.setState(function(state, props) {
return {
count: state.count + 1
}
});
}
render() {
return (
<h1>{this.state.count}</h1> //页面中将打印出2
)
}
}
这两种写法,一个是在setState中传入了object,一个是传入了function,却得到了两种不同的结果,这是什么原因造成的,这就需要我们去深入了解一下进行批量更行时都做了些什么。
批量更新
前面提到事务即将结束时,会去调用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的一个地方,要避免这个问题,需要对组件生命周期有一定的了解。在执行setState时,我们可以通过在第一个参数传入function的形式来避免类似的问题。如果大家发现有任何问题,都可以在评论中告诉我,感激不尽。
深入研究React setState的工作机制的更多相关文章
- React: React的组件状态机制
一.简介 在React中,有两个核心的默认属性,分别是state和props.state会记录组件的状态,React根据状态的变化,会对界面做相应的调整或渲染.props则是数据流向属性,React通 ...
- [Web 前端] 我不再使用React.setState的3个原因
copy from : https://blog.csdn.net/smk108/article/details/85237838 从几个月前开始,我在新开发的React组件中不再使用setState ...
- PHP自动加载__autoload的工作机制
PHP自动加载__autoload的工作机制 PHP的懒加载lazy loading 在 2011年11月12日 那天写的 已经有 4559 次阅读了 感谢 参考或原文 服务器君一共花费了 ...
- struts2的工作机制
struts2的工作机制 原文:http://eoasis.iteye.com/blog/642586 概述 本章讲述Struts2的工作原理. 读者如果曾经学习过Struts1.x或者有过Strut ...
- tomcat中Servlet的工作机制
在研究Servlet在tomcat中的工作机制前必须先看看Servlet规范的一些重要的相关规定,规范提供了一个Servlet接口,接口中包含的重要方法是init.service.destroy等方法 ...
- js异步处理工作机制
js异步处理工作机制 从基础的层面来讲,理解JavaScript的定时器是如何工作的是非常重要的.计时器的执行常常和我们的直观想象不同,那是因为JavaScript引擎是单线程的.我们先来认识一下 ...
- [翻译] Python 3.5中async/await的工作机制
Python 3.5中async/await的工作机制 多处翻译出于自己理解,如有疑惑请参考原文 原文链接 身为Python核心开发组的成员,我对于这门语言的各种细节充满好奇.尽管我很清楚自己不可能对 ...
- 大数据学习笔记——Spark工作机制以及API详解
Spark工作机制以及API详解 本篇文章将会承接上篇关于如何部署Spark分布式集群的博客,会先对RDD编程中常见的API进行一个整理,接着再结合源代码以及注释详细地解读spark的作业提交流程,调 ...
- Httpd服务入门知识-http协议版本,工作机制及http服务器应用扫盲篇
Httpd服务入门知识-http协议版本,工作机制及http服务器应用扫盲篇 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Internet与中国 Internet最早来源于美 ...
随机推荐
- JavaScript中的数据结构及实战系列(1):队列
开题 张三丰教无忌太极剑法: 还记得吗? 全都记得. 现在呢? 已经忘却了一小半. 啊,已经忘了一大半. 不坏不坏,忘得真快,那么现在呢? 已经全都忘了,忘得干干净净. 好了,你上吧. 长时间写前端代 ...
- Let's Encrypt: 为CentOS/RHEL 7下的nginx安装https支持-具体案例
环境说明: centos 7 nginx 1.10.2 前期准备 软件安装 yum install -y epel-release yum install -y certbot 创建目录及链接 方法1 ...
- Atom打造 c/c++编译环境(忙了一个上午)
众所周知 Atom是一款非常酷炫的编辑器.因为它就像上古卷轴一样,玩家可以开发各种dlc补丁,实现自己想要的效果.所以Atom 可以被你改造成自己想要的东西,可以用来写算法竞赛题目,可以开发网页,可以 ...
- (练习题)利用构造器函数实现三个小实例——不使用String()与Array()构造器和Math对象,不使用内建的方法的方法和属性。
1)在String()构造器不存在的情况下自定义一个myString()构造器函数.由于String()不存在,因此您在写构造器函数时不能使用任何属于内建String对象的方法和属性.并让你所创建的对 ...
- 转 JSON与XML转换
这两天处理模块的联调工作,在json与XML转换中出现了一些奇怪的问题,仔细究来,实为对org.json.*包知之太少.晚上baidu.google一下,找出了问题出现的原因.在模块中,使用了两个方法 ...
- lighttpd启动问题
/home/yuna/web/app/lighttpd/sbin/lighttpd -f /home/yuna/web/app/lighttpd/lighttpd.conf -m /home/yuna ...
- office web apps 整合Java web项目
之前两篇文章将服务器安装好了,项目主要的就是这么讲其整合到我们的项目中,网上大部分都是asp.net的,很少有介绍Java如何整合的,经过百度,终于将其整合到了我的项目中. 首先建个servlet拦截 ...
- JavaScript中apply与call方法
一.定义 apply:应用某一对象的一个方法,用另一个对象替换当前对象. call:调用一个对象的一个方法,以另一个对象替换当前对象. 二.apply //apply function Person( ...
- js 数字递增特效 仿支付宝我的财富 HTML5
上周五应着公司临时需求,一天的时间解决掉官网(ps:比较简单哈哈),需求里面有一个特效就是数字递增到指定的数值,其实JS写也不复杂的,但是我发现一个js小插件,这个插件轻巧简单,用起来也非常简单实用. ...
- IOS的控制器
控制器简单来说,就是用来做界面跳转的,类似于 Android 的Intent 1.创建一个控制器 控制器的常见的创建方式 )通过storyboard创建 //直接创建 NJViewController ...