全面解析Vue.nextTick實現原理


vue中有一個較為特殊的API,nextTick。根據官方文檔的解釋,它可以在DOM更新完畢之后執行一個回調,用法如下:

// 修改數據

 

vm.msg 'Hello'

 

// DOM 還沒有更新

 

Vue.nextTick(function () {

 

// DOM 更新了

 

})

盡管MVVM框架並不推薦訪問DOM,但有時候確實會有這樣的需求,尤其是和第三方插件進行配合的時候,免不了要進行DOM操作。而nextTick就提供了一個橋梁,確保我們操作的是更新后的DOM。

本文從這樣一個問題開始探索:vue如何檢測到DOM更新完畢呢?

檢索一下自己的前端知識庫,能監聽到DOM改動的API好像只有MutationObserver了,后面簡稱MO.

理解MutationObserver

MutationObserver是HTML5新增的屬性,用於監聽DOM修改事件,能夠監聽到節點的屬性、文本內容、子節點等的改動,是一個功能強大的利器,基本用法如下:

//MO基本用法

 

var observer new MutationObserver(function(){

 

  //這里是回調函數

 

  console.log('DOM被修改了!');

 

});

 

var article = document.querySelector('article');

 

observer.observer(article);

MO的使用不是本篇重點。這里我們要思考的是:vue是不是用MO來監聽DOM更新完畢的呢?

那就打開vue的源碼看看吧,在實現nextTick的地方,確實能看到這樣的代碼:

//vue@2.2.5 /src/core/util/env.js

 

if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver|| MutationObserver.toString() === '[object MutationObserverConstructor]')) {

 

  var counter 1

 

  var observer new MutationObserver(nextTickHandler)

 

  var textNode = document.createTextNode(String(counter))

 

  observer.observe(textNode{

 

      characterDatatrue

 

  })

 

  timerFunc () => {

 

    counter (counter 12

 

    textNode.data String(counter)

 

  }

 

}

簡單解釋一下,如果檢測到瀏覽器支持MO,則創建一個文本節點,監聽這個文本節點的改動事件,以此來觸發nextTickHandler(也就是DOM更新完畢回調)的執行。后面的代碼中,會執行手工修改文本節點屬性,這樣就能進入到回調函數了。

大體掃了一眼,似乎可以得到實錘了:哦!vue是用MutationObserver監聽DOM更新完畢的!

難道不感覺哪里不對勁嗎?讓我們細細想一下:

  1. 我們要監聽的是模板中的DOM更新完畢,vue為什么自己創建了一個文本節點來監聽,這有點說不通啊!

     

  2. 難道自己創建的文本節點更新完畢,就能代表其他DOM節點更新完畢嗎?這又是什么道理!

看來我們上面得出的結論並不對,這時候就需要講講js的事件循環機制了。

事件循環(Event Loop)

在js的運行環境中,我們這里光說瀏覽器吧,通常伴隨着很多事件的發生,比如用戶點擊、頁面渲染、腳本執行、網絡請求,等等。為了協調這些事件的處理,瀏覽器使用事件循環機制。

簡要來說,事件循環會維護一個或多個任務隊列(task queues),以上提到的事件作為任務源往隊列中加入任務。有一個持續執行的線程來處理這些任務,每執行完一個就從隊列中移除它,這就是一次事件循環了,如下圖所示:

我們平時用setTimeout來執行異步代碼,其實就是在任務隊列的末尾加入了一個task,待前面的任務都執行完后再執行它。

關鍵的地方來了,每次event loop的最后,會有一個UI render步驟,也就是更新DOM。標准為什么這樣設計呢?考慮下面的代碼:

for(let i=0; i<100; i++){

 

    dom.style.left = i 'px';

 

}

瀏覽器會進行100次DOM更新嗎?顯然不是的,這樣太耗性能了。事實上,這100次for循環同屬一個task,瀏覽器只在該task執行完后進行一次DOM更新。

那我們的思路就來了:只要讓nextTick里的代碼放在UI render步驟后面執行,豈不就能訪問到更新后的DOM了?

vue就是這樣的思路,並不是用MO進行DOM變動監聽,而是用隊列控制的方式達到目的。那么vue又是如何做到隊列控制的呢?我們可以很自然的想到setTimeout,把nextTick要執行的代碼當作下一個task放入隊列末尾。

然而事情卻沒這么簡單,vue的數據響應過程包含:數據更改->通知Watcher->更新DOM。而數據的更改不由我們控制,可能在任何時候發生。如果恰巧發生在repaint之前,就會發生多次渲染。這意味着性能浪費,是vue不願意看到的。

所以,vue的隊列控制是經過了深思熟慮的(也經過了多次改動)。在這之前,我們還需了解event loop的另一個重要概念,microtask.

microtask

從名字看,我們可以把它稱為微任務。對應的,task隊列中的任務也被叫做macrotask。名字相似,性質可不一樣了。

每一次事件循環都包含一個microtask隊列,在循環結束后會依次執行隊列中的microtask並移除,然后再開始下一次事件循環。

在執行microtask的過程中后加入microtask隊列的微任務,也會在下一次事件循環之前被執行。也就是說,macrotask總要等到microtask都執行完后才能執行,microtask有着更高的優先級。

microtask的這一特性,簡直是做隊列控制的最佳選擇啊!vue進行DOM更新內部也是調用nextTick來做異步隊列控制。而當我們自己調用nextTick的時候,它就在更新DOM的那個microtask后追加了我們自己的回調函數,從而確保我們的代碼在DOM更新后執行,同時也避免了setTimeout可能存在的多次執行問題。

常見的microtask有:Promise、MutationObserver、Object.observe(廢棄),以及nodejs中的process.nextTick.

咦?好像看到了MutationObserver,難道說vue用MO是想利用它的microtask特性,而不是想做DOM監聽?對嘍,就是這樣的。核心是microtask,用不用MO都行的。事實上,vue在2.5版本中已經刪去了MO相關的代碼,因為它是HTML5新增的特性,在iOS上尚有bug。

那么最優的microtask策略就是Promise了,而令人尷尬的是,Promise是ES6新增的東西,也存在兼容問題呀~ 所以vue就面臨一個降級策略。

vue的降級策略

上面我們講到了,隊列控制的最佳選擇是microtask,而microtask的最佳選擇是Promise.但如果當前環境不支持Promise,vue就不得不降級為macrotask來做隊列控制了。

macrotask有哪些可選的方案呢?前面提到了setTimeout是一種,但它不是理想的方案。因為setTimeout執行的最小時間間隔是約4ms的樣子,略微有點延遲。還有其他的方案嗎?

不賣關子了,在vue2.5的源碼中,macrotask降級的方案依次是:setImmediate、MessageChannel、setTimeout.

setImmediate是最理想的方案了,可惜的是只有IE和nodejs支持。

MessageChannel的onmessage回調也是microtask,但也是個新API,面臨兼容性的尷尬...

所以最后的兜底方案就是setTimeout了,盡管它有執行延遲,可能造成多次渲染,算是沒有辦法的辦法了。

總結

以上就是vue的nextTick方法的實現原理了,總結一下就是:

  1. vue用異步隊列的方式來控制DOM更新和nextTick回調先后執行

     

  2. microtask因為其高優先級特性,能確保隊列中的微任務在一次事件循環前被執行完畢

     

  3. 因為兼容性問題,vue不得不做了microtask向macrotask的降級方案

 

相關資料:

event loop標准

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

vue2.5的nextTick更改記錄

https://github.com/vuejs/vue/commit/6e41679a96582da3e0a60bdbf123c33ba0e86b31

源碼解析文章

https://github.com/answershuto/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM