前言
首先在學習react的時候就對setSate的實現有比較濃厚的興趣,那么對於下邊的代碼,可以快速回答嗎?
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); // 打印 me.setState({ count: me.state.count + 1 }); console.log(me.state.count); // 打印 setTimeout(function(){ 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); // 打印 }, 0); } render() { return ( <h1>{this.state.count}</h1> ) } }
這段代碼大家可能在很多地方看見過,結果是讓你匪夷所思的0,0,2,3。 大部分人相信都不知道其中的原因,首先肯定會問:
- 為什么前兩次為零,而加上setTimeout就可以打印出來?
- 為什么setTimeout打印出不同的結果?
那么請你接下來向下看,我首先說一下Batch Updata(批量更新)。如下圖:

什么事Batch Update
在一些MV*框架中,就是將一段時間內對model的修改批量更新到view的機制。比如那前端比較火的React、vue為例。
在React中,我們在componentDidMount生命周期連續調用SetState:
componentDidMount () { this.setState({ foo: 1 }) this.setState({ foo: 2 }) this.setState({ foo: 3 }) }
在沒有Batch Update的情況下,上面的操作會導致三次組件渲染,但是使用Batch Update機制下時間上只運行了一次渲染。componentDidMount中三次對model的操作被優化為一次view更新,
不必要的Vitual Dom計算被忽略,從而提高了框架的效率。
Batch Update的實現
我們想到的可能就是數據結構中的棧和隊列,比較一下還是使用一個queue來保存update,並在合適的時機對這個queue進行flush操作。那么現在有兩個問題:
- 什么時候創建這個queue
- 什么時候對這個queue進行flush
那么我們要對Reac和Vue的源碼進行分析,首先React:React中的Batch Update是通過Transaction(事務)來實現的。在React源碼關於Transaction的部分可以用一幅畫解釋:

Transaction對一個函數進行包裝,讓React有機會在一個函數執行前和執行后運行特定的邏輯,從而完成對整個Batch Update流程的控制。
簡單的說就是在要執行的函數中用事務包裹起來,在函數執行前加入initialize階段,函數執行,最后執行close階段。那么Batch Update中
在事件initialize階段,一個update queue被創建。在事件中調用setState方法時,狀態不會被立即調用,而是被push進Update queue中。
函數執行結束調用事件的close階段,Update queue會被flush,這事新的狀態才會被應用到組件上並開始后續的Virtual DOM更新,biff算法來對
model更新。
對比於React,Vue實現Batch update就簡單多了:直接借助JS中的Event Loop。(參考阮老師的http://www.ruanyifeng.com/blog/2013/10/event_loop.html)
Vue中的核心代碼就僅僅20多行,如下:
// https://github.com/vuejs/vue/blob/dev/src/core/observer/scheduler.js#L122-L148 /** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
當model被修改時,對應的watcher會被推入Update queue, 與此同時還會在異步隊列中添加一個task用於flush當前的Update queue。
這樣一來,當前的task中的其他watcher會被推進同一個Update queue中。當前task執行結束后,異步隊列下一個task執行,update queue
會被 flush,並進行后續的更新操作。
為了讓 flush 動作能在當前 Task 結束后盡可能早的開始,Vue 會優先嘗試將任務 micro-task 隊列,具體來說,在瀏覽器環境中 Vue 會優
先嘗試使用 MutationObserver API 或 Promise,如果兩者都不可用,則 fallback 到 setTimeout。
對比兩個框架可以發現 React 基於 Transition 實現的 Batch Query 是一個不依賴語言特性的通用模式,因此有更穩定可控的表現,但缺點
是無法完全覆蓋所有情況,例如對於如下代碼:
componentDidMount () { setTimeout(_ => { this.setState({ foo: 1 }) this.setState({ foo: 2 }) this.setState({ foo: 3 }) }, 0) }
由於 setTimeout 的回調函數「不受 React 控制」,其中的 setState 就無法得到優化,最終會導致 render 函數執行三次。
而 Vue 的實現則對語言特性乃至運行環境有很強的依賴,但可以更好的覆蓋各種情況:只要是在同一個 task 中的修改都可以進行 Batch Update 優化。
總結一下:
React 在這里的更新和事務機制使用比較通用的處理方式。
比如默認第一次應用初始化的時候是一次事務的進行,在用戶交互的時候是一次新的事務開始,會在同一次同步事務中標記 batchUpdate=true,這樣的做法是不破壞使用者的代碼。
然后如果是 Ajax,setTimeout 等要離開主線程進行異步操作的時候會脫離當前 UI 的事務,這時候再進入此次處理的時候 batchUpdate=false,所以才會 setState 幾次就 render 幾次。
Vue 的策略雖然在機制上雷同,但是從根本上來講是一種延遲的批量更新機制。
Angular 在這里也處理得很巧妙,利用 zone.js 對 task 進行攔截,對 JS 現有場景進行 AOP,這樣就成功的橋接了代碼。
React 的事務是純粹的 IO 模型的適配。
那么Batch Update介紹到這里 ,在下一篇我們將參考React源碼來分析setState的實現過程。
