Vue異步更新 - nextTick爲何要microtask優先?

Vue源碼解讀系列篇javascript

1、Vue異步更新隊列

(1)Vue異步更新

相信你們都知道,Vue能夠作到數據驅動視圖更新,好比咱們就簡單寫一個事件以下:html

methods: {
    tap() {
        for (let i = 0; i < 10; i++) {
            this.a = i;
        }
        this.b = 666;
    },
},
複製代碼

當咱們觸發這個事件,視圖中的ab確定會發現一些變化。vue

那咱們思考一下,Vue是如何管理這個變化的過程?好比上面這個案例,a被循環了10次,那Vue會去渲染視圖10次嗎?顯然不會,畢竟這個性能代價很是大。畢竟咱們只須要a最後一次的賦值。java

實際上Vue是異步更新視圖的,也就是說會等這個tap事件執行完,檢查發現只須要更新ab,而後再一次性更新,避免無效的更新。node

Vue官方文檔也印證了咱們的想法,以下:react

Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。git

以上能夠詳見Vue官方文檔 - 異步更新隊列github

(2)派發更新中的異步隊列

Vue通知視圖更新,是經過dep.notify,相信你讀到這裏確定是瞭解Vue響應式原理的。那麼來查看下dep.notify都作了什麼?耐心點,離真相愈來愈近了。面試

// dep.js
notify () {
    const subs = this.subs.slice();
    // 循環通知全部watcher更新
    for (let i = 0, l = subs.length; i < l; i++) {
        subs[i].update()
    }
}
複製代碼

首先循環通知全部watcher更新,咱們發現watcher執行了update方法。瀏覽器

// watcher.js
update () {
    if (this.lazy) {
        // 若是是計算屬性
        this.dirty = true
    } else if (this.sync) {
        // 若是要同步更新
        this.run()
    } else {
        // 進入更新隊列
        queueWatcher(this)
    }
}
複製代碼

update方法首先判斷是否是計算屬性或開發者定義了同步更新,這些咱們先不看,直接進入正題,進入異步隊列方法queueWatcher

那麼再來看下queueWatcher,我省略了絕大部分代碼,畢竟代碼是枯燥的,爲了方便你們理解,都是一些思路性代碼。

export function queueWatcher (watcher: Watcher) {
    // 獲取watcherid
    const id = watcher.id
    if (has[id] == null) {
        // 保證只有一個watcher,避免重複
        has[id] = true
        
        // 推入等待執行的隊列
        queue.push(watcher)
      
        // ...省略細節代碼
    }
    // 將全部更新動做放入nextTick中,推入到異步隊列
    nextTick(flushSchedulerQueue)
}

function flushSchedulerQueue () {
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        watcher.run()
        // ...省略細節代碼
    }
}
複製代碼

經過上述代碼能夠看出咱們將全部要更新的watcher隊列放入了nextTick中。 nextTick的官方解讀爲:

在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。

這裏的描述其實限制了nextTick的技能,實際上nextTick就是一個異步方法,也許和你使用的setTimeout沒有太大的區別。

那來看下nextTick的源碼究竟作了什麼?

2、nextTick源碼淺析

nextTick源碼不多,翻來翻去沒幾行,可是我也不打算展開講,由於看代碼真的很枯燥。 下面的代碼只有幾行,其實你能夠選擇跳過看結論。

// timerFunc就是nextTick傳進來的回調等... 細節不展開
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
    }
    isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    // 當原生 Promise 不可用時,timerFunc 使用原生 MutationObserver
    // MutationObserver不要在乎它的功能,其實就是個能夠達到微任務效果的備胎
)) {
    timerFunc = () => {
        // 使用 MutationObserver
    }
    isUsingMicroTask = true

} else if (typeof setImmediate !== 'undefined' &&  isNative(setImmediate)) {
    // 若是原生 setImmediate 可用,timerFunc 使用原生 setImmediate
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    // 最後的倔強,timerFunc 使用 setTimeout
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}

複製代碼

總結就是Promise > MutationObserver > setImmediate > setTimeout

果真和setTimeout沒有太大的區別~

再總結一下優先級:microtask (jobs) 優先。

nextTick源碼爲何要microtask優先?再理解這個問題答案以前,咱們還要複習eventLoop知識。

3、eventLoop

(1)任務隊列

用2張圖帶你們簡單回憶一下,可是就不細講了,你們能夠自行查找資料。

eventLoop

  • 咱們的同步任務在主線程上運行會造成一個執行棧。
  • 若是碰到異步任務,好比setTimeoutonClick等等的一些操做,咱們會將他的執行結果放入隊列,此期間主線程不阻塞。
  • 等到主線程中的全部同步任務執行完畢,就會經過event loop在隊列裏面從頭開始取,在執行棧中執行 event loop永遠不會斷。
  • 以上的這一整個流程就是Event Loop(事件循環機制)。

(2)微任務、宏任務

eventLoop、微任務、宏任務

  • 每次執行棧的同步任務執行完畢,就會去任務隊列中取出完成的異步任務,可是隊列中又分爲微任務microtask和宏任務tasks隊列
  • 等到把全部的微任務microtask都執行完畢,注意是全部的,他纔會從宏任務tasks隊列中取事件。
  • 等到把隊列中的事件取出一個,放入執行棧執行完成,就算一次循環結束。
  • 以後event loop還會繼續循環,他會再去微任務microtask執行全部的任務,而後再從宏任務tasks隊列裏面取一個,如此反覆循環。

4、nextTick爲何要儘量的microtask優先?

簡單的回憶了eventLoop、微任務、宏任務後,咱們還要再拋出一個結論。

執行順序

咱們發現,原來在執行微任務以後還會執行渲染操做!!!(固然並非每次都會,但至少順序咱們是能夠確定的)。

  • 在一輪event loop中屢次修改同一dom,只有最後一次會進行繪製。
  • 渲染更新(Update the rendering)會在event loop中的tasksmicrotasks完成後進行,但並非每輪event loop都會更新渲染,這取決因而否修改了dom和瀏覽器以爲是否有必要在此時當即將新狀態呈現給用戶。若是在一幀的時間內(時間並不肯定,由於瀏覽器每秒的幀數總在波動,16.7ms只是估算並不許確)修改了多處dom,瀏覽器可能將變更積攢起來,只進行一次繪製,這是合理的。
  • 若是但願在每輪event loop都即時呈現變更,可使用requestAnimationFrame

這裏我拋出結論,緣由和理論知識能夠看這篇文章 從event loop規範探究javaScript異步及瀏覽器更新渲染時機 ,這位大神寫的很好。

不知道你們有沒有猜出【nextTick爲何要儘量的microtask優先?】 這裏又盜了大神的圖,event loop的大體循環過程:

event loops

假設如今執行到某個 task,咱們對批量的dom進行異步修改,咱們將此任務插進tasks,也就是用宏任務實現。

task

顯而易見,這種狀況下若是task裏排隊的隊列比較多,同時遇到屢次的微任務隊列執行完。那頗有可能觸發屢次瀏覽器渲染,可是依舊沒有執行咱們真正的修改dom任務。

這種狀況,不只會延遲視圖更新,帶來性能問題。還有可能致使視圖上一些詭異的問題。 所以,此任務插進microtasks

能夠看到若是 task隊列若是有大量的任務等待執行時,將 dom的變更做爲 microtasks而不是宏任務( task)能更快的將變化呈現給用戶。

總結

之因此講這篇文章,是由於在最近在讀Vue的源碼,我看的是2.6.10, 發現nextTick和2.5版本的實現不太同樣。你們能夠看下這位大佬的文章 Vue.js 升級踩坑小記

文章內容基本都是在其餘大佬的基礎上進行的理解,講錯的你們能夠批評指正~

參考文章

文章有一些結論直接參考其餘文章,本身實在是懶得寫啦~~

侵權刪 ^^

相關文章
相關標籤/搜索