原文連接:https://www.cnblogs.com/jasonlzy/p/8046118.html
前言
首先在學(xué)習(xí)react的時候就對setSate的實現(xiàn)有比較濃厚的興趣挂谍,那么對于下邊的代碼,可以快速回答嗎迷帜?
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">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>
)
}
}</pre>
](javascript:void(0); "復(fù)制代碼")
這段代碼大家可能在很多地方看見過输吏,結(jié)果是讓你匪夷所思的0权旷,0,2贯溅,3拄氯。 大部分人相信都不知道其中的原因躲查,首先肯定會問:
- 為什么前兩次為零,而加上setTimeout就可以打印出來坤邪?
- 為什么setTimeout打印出不同的結(jié)果熙含?
那么請你接下來向下看,我首先說一下Batch Updata(批量更新)艇纺。如下圖:
什么事Batch Update
在一些MV*框架中怎静,就是將一段時間內(nèi)對model的修改批量更新到view的機制。比如那前端比較火的React黔衡、vue為例蚓聘。
在React中,我們在componentDidMount生命周期連續(xù)調(diào)用SetState:
<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">componentDidMount () { this.setState({ foo: 1 }) this.setState({ foo: 2 }) this.setState({ foo: 3 })
}</pre>
在沒有Batch Update的情況下盟劫,上面的操作會導(dǎo)致三次組件渲染夜牡,但是使用Batch Update機制下時間上只運行了一次渲染。componentDidMount中三次對model的操作被優(yōu)化為一次view更新侣签,
不必要的Vitual Dom計算被忽略塘装,從而提高了框架的效率。
Batch Update的實現(xiàn)
我們想到的可能就是數(shù)據(jù)結(jié)構(gòu)中的棧和隊列影所,比較一下還是使用一個queue來保存update蹦肴,并在合適的時機對這個queue進行flush操作。那么現(xiàn)在有兩個問題:
- 什么時候創(chuàng)建這個queue
- 什么時候?qū)@個queue進行flush
那么我們要對Reac和Vue的源碼進行分析猴娩,首先React:React中的Batch Update是通過Transaction(事務(wù))來實現(xiàn)的阴幌。在React源碼關(guān)于Transaction的部分可以用一幅畫解釋:
Transaction對一個函數(shù)進行包裝,讓React有機會在一個函數(shù)執(zhí)行前和執(zhí)行后運行特定的邏輯卷中,從而完成對整個Batch Update流程的控制矛双。
簡單的說就是在要執(zhí)行的函數(shù)中用事務(wù)包裹起來,在函數(shù)執(zhí)行前加入initialize階段蟆豫,函數(shù)執(zhí)行议忽,最后執(zhí)行close階段。那么Batch Update中
在事件initialize階段十减,一個update queue被創(chuàng)建徙瓶。在事件中調(diào)用setState方法時,狀態(tài)不會被立即調(diào)用嫉称,而是被push進Update queue中。
函數(shù)執(zhí)行結(jié)束調(diào)用事件的close階段灵疮,Update queue會被flush织阅,這事新的狀態(tài)才會被應(yīng)用到組件上并開始后續(xù)的Virtual DOM更新,biff算法來對
model更新震捣。
對比于React荔棉,Vue實現(xiàn)Batch update就簡單多了:直接借助JS中的Event Loop闹炉。(參考阮老師的http://www.ruanyifeng.com/blog/2013/10/event_loop.html)
Vue中的核心代碼就僅僅20多行,如下:
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">// 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)
}
}
}</pre>
](javascript:void(0); "復(fù)制代碼")
當model被修改時润樱,對應(yīng)的watcher會被推入Update queue渣触, 與此同時還會在異步隊列中添加一個task用于flush當前的Update queue。
這樣一來壹若,當前的task中的其他watcher會被推進同一個Update queue中嗅钻。當前task執(zhí)行結(jié)束后,異步隊列下一個task執(zhí)行店展,update queue
會被 flush养篓,并進行后續(xù)的更新操作。
為了讓 flush 動作能在當前 Task 結(jié)束后盡可能早的開始赂蕴,Vue 會優(yōu)先嘗試將任務(wù) micro-task 隊列柳弄,具體來說,在瀏覽器環(huán)境中 Vue 會優(yōu)
先嘗試使用 MutationObserver API 或 Promise概说,如果兩者都不可用碧注,則 fallback 到 setTimeout。
對比兩個框架可以發(fā)現(xiàn) React 基于 Transition 實現(xiàn)的 Batch Query 是一個不依賴語言特性的通用模式糖赔,因此有更穩(wěn)定可控的表現(xiàn)萍丐,但缺點
是無法完全覆蓋所有情況,例如對于如下代碼:
[](javascript:void(0); "復(fù)制代碼")
<pre style="margin-top: 0px; margin-bottom: 0px; white-space: pre-wrap; word-wrap: break-word; font-family: "Courier New" !important; font-size: 12px !important;">componentDidMount () {
setTimeout(_ => { this.setState({ foo: 1 }) this.setState({ foo: 2 }) this.setState({ foo: 3 })
}, 0)
}</pre>
](javascript:void(0); "復(fù)制代碼")
由于 setTimeout 的回調(diào)函數(shù)「不受 React 控制」挂捻,其中的 setState 就無法得到優(yōu)化碉纺,最終會導(dǎo)致 render 函數(shù)執(zhí)行三次。
而 Vue 的實現(xiàn)則對語言特性乃至運行環(huán)境有很強的依賴刻撒,但可以更好的覆蓋各種情況:只要是在同一個 task 中的修改都可以進行 Batch Update 優(yōu)化骨田。
總結(jié)一下:
React 在這里的更新和事務(wù)機制使用比較通用的處理方式。
比如默認第一次應(yīng)用初始化的時候是一次事務(wù)的進行声怔,在用戶交互的時候是一次新的事務(wù)開始态贤,會在同一次同步事務(wù)中標記 batchUpdate=true,這樣的做法是不破壞使用者的代碼醋火。
然后如果是 Ajax悠汽,setTimeout 等要離開主線程進行異步操作的時候會脫離當前 UI 的事務(wù),這時候再進入此次處理的時候 batchUpdate=false芥驳,所以才會 setState 幾次就 render 幾次柿冲。
Vue 的策略雖然在機制上雷同,但是從根本上來講是一種延遲的批量更新機制兆旬。
Angular 在這里也處理得很巧妙假抄,利用 zone.js 對 task 進行攔截,對 JS 現(xiàn)有場景進行 AOP,這樣就成功的橋接了代碼宿饱。
React 的事務(wù)是純粹的 IO 模型的適配熏瞄。
那么Batch Update介紹到這里 ,在下一篇我們將參考React源碼來分析setState的實現(xiàn)過程谬以。