关于React setState的实现原理(二)
React中的Transaction
大家学过sql server的都知道我们可以批量处理sql语句,原理其实都是基于上一篇我们说的Datch Update机制。当所有的操作均执行成功,才会执行修改操作;若有一个操作失败,则执行rollback(回滚)。
在React中,我们介绍过事件会在函数前后执行自己的逻辑,具体就是调用perform方法进入一个事件,这个方法会传入一个method参数。执行perform时先执行initializeAll方法按照一定顺序执行一系列的initialize
惭怍,然后执行传入的method,method执行完后,就执行closeAll方法按照一定顺序执行一系列的close操作。注意一种事件不能同时开启,否则会抛出异常。给一个例子是实现事件:
var Transaction = require('./Transaction');
// 我们自己定义的 Transaction
var MyTransaction = function() {
// do sth.
};
Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [{
initialize: function() {
console.log('before method perform');
},
close: function() {
console.log('after method perform');
}
}];
};
});
var transaction = new MyTransaction();
var testMethod = function() {
console.log('test');
}
transaction.perform(testMethod);
// before method perform
// test
// after method perform
具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。
当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。
组件调用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);
}
}
};
从代码中可以看出,这个batchedUpdate是第一次调用alreadyBatchingUpdates是false(储存起来了),回去执行transaction.perform(method)(前边说过perform执行会进入一个时间),这样就进入第一个事务,
这个事务是啥我们现在不用管,我们只需要知道这个transaction是ReactDefaultBatchingStrategyTransaction的实例,它代表了其中一类事务的执行。然后会执行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;
}
}
上面的代码中,个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,即意味着事务结束。
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) { //上一个事件结束执行过isBatchedUpdates=false,所以进入if中
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
接下来再调用setState时(在setTimeout中,前文说过一步操作不会在主线程,我理解是在主线程结束才会执行,此时的主线程事件已经结束),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,却得到了两种不同的结果,这是什么原因造成的,这就需要我们去深入了解一下进行批量更行时都做了些什么。
关于React setState的实现原理(二)的更多相关文章
- 关于React setState的实现原理(三)
前面提到事务即将结束时,会去调用FLUSH_BATCHED_UPDATES的flushBatchedUpdates方法执行批量更新,该方法会去遍历dirtyComponents,对每一项执行perfo ...
- 关于React setState的实现原理(一)
前言 首先在学习react的时候就对setSate的实现有比较浓厚的兴趣,那么对于下边的代码,可以快速回答吗? class Root extends React.Component { constru ...
- 动态修改JS对象的值及React setState
一.在JS里使用(非ES6) 实现场景: 给一个空对象填充某一指定数组内的值 并随机生成数量 const fruit = ['apple', 'banana', 'orange'] let fruit ...
- React Native 入门到原理(详解)
抛砖引玉(帮你更好的去理解怎么产生的 能做什么) 砖一.动态配置 由于 AppStore 审核周期的限制,如何动态的更改 app 成为了永恒的话题.无论采用何种方式,我们的流程总是可以归结为以下三部曲 ...
- word2vec原理(二) 基于Hierarchical Softmax的模型
word2vec原理(一) CBOW与Skip-Gram模型基础 word2vec原理(二) 基于Hierarchical Softmax的模型 word2vec原理(三) 基于Negative Sa ...
- juc线程池原理(二):ThreadPoolExecutor的成员变量介绍
概要 线程池的实现类是ThreadPoolExecutor类.本章,我们通过分析ThreadPoolExecutor类,来了解线程池的原理. ThreadPoolExecutor数据结构 Thread ...
- react页面内嵌微信二维码 和 自定义样式 以及 微信网页共用unionId问题
在react页面内嵌“微信二维码”,实现PC端通过微信扫码进行登录.首先去微信开放平台注册一个账号,创建一个网站应用,提交网站备案审核,获取appid和appsecret:其他开发流程根据微信文档来进 ...
- 并发之AQS原理(二) CLH队列与Node解析
并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...
- 深入理解React:事件机制原理
目录 序言 DOM事件流 事件捕获阶段.处于目标阶段.事件冒泡阶段 addEventListener 方法 React 事件概述 事件注册 document 上注册 回调函数存储 事件分发 小结 参考 ...
随机推荐
- php中Redis的扩展
首先要下载 php_redis.dll 和 php_igbinary.dll 在官网(https://windows.php.net/downloads/pecl/snaps/redis/3.1.4/ ...
- 3.numpy_array数组
官方文档:www.numpy.org.cn Numpy 数组及其索引 先导入numpy: from numpy import * 产生数组 从列表产生数组: lst = [0, 1, 2, 3] a ...
- 【洛谷p1106】删数问题
(洛谷t2755暂时过不去了) 删数问题[传送门] 洛谷算法标签: emmmm……删数问题又牵扯到了字符串.因为毕竟高精度的数240位呢!要是输入一个整型,要码240行来求出每一位……怕是还没求出来就 ...
- mac 下配置Android sdk
1.通过homebrew进行安装 地址:https://brew.sh/index_zh-cn.html 2.homebrew install android sdk 3.找到路径如: /Users ...
- spring boot 多数据源配置与使用
在介绍使用JdbcTemplate和Spring-data-jpa时,都使用了单数据源.在单数据源的情况下,Spring Boot的配置非常简单,只需要在application.properties文 ...
- MVC实战之排球计分(三)—— 模型类的设计与实现
此软件使用的数据库连接方式code first 由EF框架产生数据库. code first需要对模型类设计和实现.模型类是现实实体在计算机中的表示.它贯穿于整个架构, 负担着在各层次及模块间传递数据 ...
- ORACLE中ESCAPE关键字用法
ESCAPE用法 1.使用 ESCAPE 关键字定义转义符: 在模式中,当转义符置于通配符之前时,该通配符就解释为普通字符. 2.ESCAPE 'escape_character' 允许在字符串中搜 ...
- java.lang.Exception: DEBUG STACK TRACE for PoolBackedDataSource.close().
java.lang.Exception: DEBUG STACK TRACE for PoolBackedDataSource.close(). java.lang.Exception: DEBUG ...
- WannaCry(永恒之蓝)病毒处理方法
1.直接关闭server服务 打开cmd执行关闭server服务即可: net stop server 控制面板--管理工具--服务里手动关掉 2.防火墙限制445端口 3.打补丁 [KB401259 ...
- CAS5.3-下载安装
cas版本:cas5.3 环境准备:1.JDK8:2.Apache Maven:3.Apache Tomcat:4.git 1.通过git工具(本文使用的是TortoiseGit,也可用IDE或者gi ...