参考原文:react
React组件的componentDidMount事件里使用setState方法,会有一些有趣的事情:函数
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
复制代码
运行这段代码,咱们能够看到屏幕里打印的是0、0、二、3。ui
这好像跟咱们想象中的不大同样,咱们先看下setState流程图,看看这个方法里发生了什么事情this
咱们能够看到,若是处于批量更新阶段内,就会把全部更改的操做存入pending队列,当咱们已经完成批量更新收集阶段,咱们读取pengding队列里的操做,一次性处理并更新state。那么根据上面的执行结果,咱们大概能够猜到,前面两个setState操做应该是恰好处于批量更新阶段,这两个操做都被收集到队列里,即state在这个阶段里暂时不会被更改,因此仍是保留原始值0。spa
当setTiemout的时候,跳出了当前执行的任务队列,估计相应也跳出了批量更新阶段,因此致使如今的操做会当即体如今state(此时通过上面的更改,state已经变成了1)里。因此后面两个操做会致使state值陆续变成二、3。若是用任务队列的方式这么理解,好像是说得通,那么咱们关心的是为何componentDidMount事件里就处于batch update了,也就是batch update实际上是什么东西?3d
查看React源码里,setState里源码对应下面这段:code
function enqueueUpdate(component) {
// ...
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
dirtyComponents.push(component);
}
复制代码
也就是由batchingStrategy的isBatchingUpdates属性来决定当前是否处于批量更新阶段,而后再由batchingStrategy来执行批量更新。component
那么batchingStrategy是什么?其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法。下面是一段简化的定义代码:
var batchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
// ...
batchingStrategy.isBatchingUpdates = true;
transaction.perform(callback, null, a, b, c, d, e);
}
};
复制代码
注意 batchingStrategy 中的 batchedUpdates 方法中,有一个 transaction.perform 调用。这就引出了本文要介绍的核心概念 —— Transaction(事务)。
在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 Transaction 的做用。
/*
* <pre>
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
* </pre>
*/
复制代码
咱们能够看到,其实在内部是经过将须要执行的method使用wrapper封装起来,再托管给Transaction提供的perform方法执行,由Transaction统一来初始化和关闭每一个wrapper。
那么 Transaction 跟 setState 的不一样表现有什么关系呢?首先咱们把 4 次 setState 简单归类,前两次属于一类,由于他们在同一次调用栈中执行;setTimeout 中的两次 setState 属于另外一类,缘由同上。让咱们看看componentDidMout 中 setState 调用栈:
而setTimeout 中 setState 的调用栈以下:
咱们能够看到,里边的setState是包裹在batchedUpdates的Transaction里执行的。那此次 batchedUpdate 方法,又是谁调用的呢?让咱们往前再追溯一层,原来是ReactMount.js中的_renderNewRootComponent方法。也就是说,整个将React组件渲染到DOM中的过程就处于一个大的Transaction中。
接下来的解释就瓜熟蒂落了,由于在componentDidMount中调用setState时,batchingStrategy的isBatchingUpdates已经被设为true,因此两次setState的结果并无当即生效,而是被放进了 dirtyComponents 中。这也解释了两次打印this.state.val都是 0 的缘由,新的state尚未被应用到组件中。
再反观setTimeout中的两次setState,由于没有前置的batchedUpdate调用,因此batchingStrategy的isBatchingUpdates标志位是false,也就致使了新的state立刻生效,没有走到dirtyComponents分支。也就是,setTimeout中第一次setState时,this.state.val为 1,而setState 完成后打印时this.state.val变成了 2。第二次setState同理。
咱们再看看下面的例子
var Example = React.createClass({
getInitialState: function() {
return {
clicked: 0
};
},
handleClick: function() {
this.setState({clicked: this.state.clicked + 1});
this.setState({clicked: this.state.clicked + 1});
console.log(this.state.clicked)
},
render: function() {
return <button onClick={this.handleClick}>{this.state.clicked}</button>;
}
});
复制代码
执行以后,咱们能够看到,其实只调用了一遍setState,而且this.state.clicked等于0
上面的流程图中只保留了部分核心的过程,看到这里你们应该明白了,全部的 batchUpdate 功能都是经过托管给transaction实现的。this.setState 调用后,新的 state 并无立刻生效,而是经过 ReactUpdates.batchedUpdate 方法存入临时队列中。当外层的transaction 完成后,才调用ReactUpdates.flushBatchedUpdates 方法将全部的临时 state merge 并计算出最新的 props 及 state。
纵观 React 源码,使用 Transaction 之处很是之多,React 源码注释中也列举了不少可使用 Transaction 的地方,好比
值得一提的是,React 还将 batchUpdate 方法暴露了出来:
var batchedUpdates = require('react-dom').unstable_batchedUpdates;
复制代码
当你须要在一些非 DOM 事件回调的函数中屡次调用 setState 等方法时,能够将你的逻辑封装后调用 batchedUpdates 执行,以此保证 render 方法不会被屡次调用。