Vue的異步更新實現原理
關注公眾號 程序員成長指北,回復“加群”
加入我們一起學習,天天進步

作者:Liqiuyue
鏈接:https://juejin.cn/post/6908264284032073736
最近面試總是會被問到這么一個問題:在使用vue的時候,將for循環(huán)中聲明的變量i從1增加到100,然后將i展示到頁面上,頁面上的i是從1跳到100,還是會怎樣?答案當然是只會顯示100,并不會有跳轉的過程。
怎么可以讓頁面上有從1到100顯示的過程呢,就是用setTimeout或者Promise.then等方法去模擬。
講道理,如果不在vue里,單獨運行這段程序的話,輸出一定是從1到100,但是為什么在vue中就不一樣了呢?
for(let?i=1;?i<=100;?i++){
?console.log(i);
}
這就涉及到Vue底層的異步更新原理,也要說一說nextTick的實現。不過在說nextTick之前,有必要先介紹一下JS的事件運行機制。
JS運行機制
眾所周知,JS是基于事件循環(huán)的單線程的語言。執(zhí)行的步驟大致是:
當代碼執(zhí)行時,所有同步的任務都在主線程上執(zhí)行,形成一個執(zhí)行棧; 在主線程之外還有一個任務隊列(task queue),只要異步任務有了運行結果就在任務隊列中放置一個事件; 一旦執(zhí)行棧中所有同步任務執(zhí)行完畢(主線程代碼執(zhí)行完畢),此時主線程不會空閑而是去讀取任務隊列。此時,異步的任務就結束等待的狀態(tài)被執(zhí)行。 主線程不斷重復以上的步驟。
我們把主線程執(zhí)行一次的過程叫一個tick,所以nextTick就是下一個tick的意思,也就是說用nextTick的場景就是我們想在下一個tick做一些事的時候。
所有的異步任務結果都是通過任務隊列來調度的。而任務分為兩類:宏任務(macro task)和微任務(micro task)。它們之間的執(zhí)行規(guī)則就是每個宏任務結束后都要將所有微任務清空。常見的宏任務有setTimeout/MessageChannel/postMessage/setImmediate,微任務有MutationObsever/Promise.then。
想要透徹學習事件循環(huán),推薦Jake在JavaScript全球開發(fā)者大會的演講,保證講懂!
nextTick原理
派發(fā)更新
大家都知道vue的響應式的靠依賴收集和派發(fā)更新來實現的。在修改數組之后的派發(fā)更新過程,會觸發(fā)setter的邏輯,執(zhí)行dep.notify():
//?src/core/observer/watcher.js
class?Dep?{
?notify()?{
?????//subs是Watcher的實例數組
?????const?subs?=?this.subs.slice()
????????for(let?i=0,?l=subs.length;?i?????????subs[i].update()
????????}
????}
}
遍歷subs里每一個Watcher實例,然后調用實例的update方法,下面我們來看看update是怎么去更新的:
class?Watcher?{
?update()?{
?????...
?????//各種情況判斷之后
????????else{
?????????queueWatcher(this)
????????}
????}
}
update執(zhí)行后又走到了queueWatcher,那就繼續(xù)去看看queueWatcher干啥了(希望不要繼續(xù)套娃了:
//queueWatcher?定義在?src/core/observer/scheduler.js
const?queue:?Array?=?[]
let?has:?{?[key:?number]:??true?}?=?{}
let?waiting?=?false
let?flushing?=?false
let?index?=?0
export?function?queueWatcher(watcher:?Watcher)?{
?const?id?=?watcher.id
????//根據id是否重復做優(yōu)化
????if(has[id]?==?null){
?????has[id]?=?true
????????if(!flushing){
?????????queue.push(watcher)
????????}else{
?????????let?i=queue.length?-?1
????????????while(i?>?index?&&?queue[i].id?>?watcher.id){
?????????????i--
????????????}
????????????queue.splice(i?+?1,?0,?watcher)
????????}
???????
?????if(!waiting){
??????waiting?=?true
?????????//flushSchedulerQueue函數:?Flush?both?queues?and?run?the?watchers
?????????nextTick(flushSchedulerQueue)
?????}
????}
}
這里queue在pushwatcher時是根據id和flushing做了一些優(yōu)化的,并不會每次數據改變都觸發(fā)watcher的回調,而是把這些watcher先添加到?個隊列?,然后在nextTick后執(zhí)?flushSchedulerQueue。
flushSchedulerQueue函數是保存更新事件的queue的一些加工,讓更新可以滿足Vue更新的生命周期。
這里也解釋了為什么for循環(huán)不能導致頁面更新,因為for是主線程的代碼,在一開始執(zhí)行數據改變就會將它push到queue里,等到for里的代碼執(zhí)行完畢后i的值已經變化為100時,這時vue才走到nextTick(flushSchedulerQueue)這一步。
nextTick源碼
接著打開vue2.x的源碼,目錄core/util/next-tick.js,代碼量很小,加上注釋才110行,是比較好理解的。
const?callbacks?=?[]
let?pending?=?false
export?function?nextTick?(cb?:?Function,?ctx?:?Object)?{
??let?_resolve
??callbacks.push(()?=>?{
????if?(cb)?{
??????try?{
????????cb.call(ctx)
??????}?catch?(e)?{
????????handleError(e,?ctx,?'nextTick')
??????}
????}?else?if?(_resolve)?{
??????_resolve(ctx)
????}
??})
??if?(!pending)?{
????pending?=?true
????timerFunc()
??}
首先將傳入的回調函數cb(上節(jié)的flushSchedulerQueue)壓入callbacks數組,最后通過timerFunc函數一次性解決。
let?timerFunc
if?(typeof?Promise?!==?'undefined'?&&?isNative(Promise))?{
??const?p?=?Promise.resolve()
??timerFunc?=?()?=>?{
????p.then(flushCallbacks)
????if?(isIOS)?setTimeout(noop)
????}
??isUsingMicroTask?=?true
}?else?if?(!isIE?&&?typeof?MutationObserver?!==?'undefined'?&&?(
??isNative(MutationObserver)?||
??//?PhantomJS?and?iOS?7.x
??MutationObserver.toString()?===?'[object?MutationObserverConstructor]'
))?{
??let?counter?=?1
??const?observer?=?new?MutationObserver(flushCallbacks)
??const?textNode?=?document.createTextNode(String(counter))
??observer.observe(textNode,?{
????characterData:?true
??})
??timerFunc?=?()?=>?{
????counter?=?(counter?+?1)?%?2
????textNode.data?=?String(counter)
??}
??isUsingMicroTask?=?true
}?else?if?(typeof?setImmediate?!==?'undefined'?&&?isNative(setImmediate))?{
??timerFunc?=?()?=>?{
????setImmediate(flushCallbacks)
??}
}?else?{
??timerFunc?=?()?=>?{
????setTimeout(flushCallbacks,?0)
??}
}
timerFunc下面一大片if else是在判斷不同的設備和不同情況下選用哪種特性去實現異步任務:優(yōu)先檢測是否原生?持Promise,不?持的話再去檢測是否?持MutationObserver,如果都不行就只能嘗試宏任務實現,首先是setImmediate,這是?個?版本 IE 和 Edge 才?持的特性,如果都不?持的話最后就會降級為 setTimeout 0。
這?使?callbacks?不是直接在nextTick中執(zhí)?回調函數的原因是保證在同?個 tick 內多次執(zhí)?nextTick,不會開啟多個異步任務,?把這些異步任務都壓成?個同步任務,在下?個 tick 執(zhí)?完畢。
nextTick使用
nextTick不僅是vue的源碼文件,更是vue的一個全局API。下面來看看怎么使用吧。
當設置 vm.someData = 'new value',該組件不會立即重新渲染。當刷新隊列時,組件會在下一個事件循環(huán)tick中更新。多數情況我們不需要關心這個過程,但是如果你想基于更新后的 DOM 狀態(tài)來做點什么,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發(fā)人員使用數據驅動的方式思考,避免直接接觸 DOM,但是有時我們必須要這么做。為了在數據變化之后等待 Vue 完成更新 DOM,可以在數據變化之后立即使用Vue.nextTick(callback)。這樣回調函數將在 DOM 更新完成后被調用。
官網用例:
<div?id="example">{{message}}div>
var?vm?=?new?Vue({
??el:?'#example',
??data:?{
????message:?'123'
??}
})
vm.message?=?'new?message'?//?更改數據
vm.$el.textContent?===?'new?message'?//?false
Vue.nextTick(function?()?{
??vm.$el.textContent?===?'new?message'?//?true
})
并且因為$nextTick() 返回一個 Promise 對象,所以也可以使用async/await 語法去處理事件,非常方便。
相關文章
最后
??愛心三連擊 1.看到這里了就點個在看支持下吧,你的「點贊,在看」是我創(chuàng)作的動力。
2.關注公眾號
程序員成長指北,回復「1」加入高級前端交流群!「在這里有好多 前端?開發(fā)者,會討論?前端 Node 知識,互相學習」!3.也可添加微信【ikoala520】,一起成長。
