參考原文: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 方法不會被屢次調用。