Vue 倡導開發者儘可能不直接操做 DOM,但有的時候因爲各類需求讓開發者不得不這樣作,因而 nextTick 的實現就是讓開發者在修改數據後,可以在數據更新到 DOM 後才執行對應的函數,從而獲取最新的 DON 數據。html
那麼如何實現 nextTick呢,咱們首先能夠想到的是利用 setTimeout 的異步回調來實現,不過因爲各個瀏覽器的不一樣,setTimeout 的延遲很高,所以在 nextTick 中只做爲最後的備胎,首選的方案則是 MutationObserver(在後面的內容中 MO 表明 MutationObserver)node
export const nextTick = (function () { var callbacks = [] var pending = false var timerFunc function nextTickHandler () { pending = false var copies = callbacks.slice(0) callbacks = [] for (var i = 0; i < copies.length; i++) { copies[i]() } } /* istanbul ignore if */ if (typeof MutationObserver !== 'undefined') { // 首選 MutationObserver var counter = 1 var observer = new MutationObserver(nextTickHandler) // 聲明 MO 和回調函數 var textNode = document.createTextNode(counter) observer.observe(textNode, { // 監聽 textNode 這個文本節點 characterData: true // 一旦文本改變則觸發回調函數 nextTickHandler }) timerFunc = function () { counter = (counter + 1) % 2 // 每次執行 timeFunc 都會讓文本在 1 和 0 間切換 textNode.data = counter } } else { timerFunc = setTimeout // 若是不支持 MutationObserver, 退選 setTimeout } return function (cb, ctx) { var func = ctx ? function () { cb.call(ctx) } : cb callbacks.push(func) if (pending) return pending = true timerFunc(nextTickHandler, 0) } })()
MO 給開發者提供了一種能在某個範圍內的DOM數發生變化時做出適當反應的能力 ——MDNgit
用人話說是開發者能經過它建立一個觀察者對象,這個對象會監聽某個DOM元素,並在它的DOM樹發生變化時執行咱們提供的回調函數。es6
具體參考這個 DEMOgithub
比較特別的是實例化的時候須要先傳入回調函數:web
var observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { console.log(mutation.type); }) })
而後才配置觀察選項,包括觀察節點和觀察的屬性:api
// 選擇目標節點 var target = document.querySelector('#some-id'); // 配置觀察選項: var config = { attributes: true, childList: true, characterData: true } // 傳入目標節點和觀察選項 observer.observe(target, config); // 隨後,你還能夠中止觀察 observer.disconnect();
對於老版本的谷歌和火狐,則須要使用帶前綴的 MO:瀏覽器
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver
那麼爲何優選使用 MutationObserver呢?app
一開始覺得是 MO 就是用來監聽 DOM 變化的,那麼使用 textnode 模擬 DOM 變化再利用 MO 來監聽觸發從而實現 nextTick 不就很適合,直到了解看到了知乎上的問答才知道是由於 MO 會比 setTimeout 早執行的緣故,
這裏須要瞭解JS的運行運行機制(從新刷新了個人三觀), JS 的事件運行機制執行的時候會區分 task 和 microtask, 引擎在每一個 task 執行完畢,並在從隊列裏取下一個task來執行以前, 執行完全部的 microtask 隊列中的 microtask
(task 和 microtask 摘自 https://jakearchibald.com/)
setTimeout 回調會被分配到一個新的task中等待執行,而 Promise 的 resolver、MO 的 回調都會被分配到 microtask 的隊列中,因此會比 setTimout 先執行
除了比 setTimout 快以外,還有 渲染性能 的問題,根據HTML Standard, 每一個 task 運行完之後, UI 都會從新渲染,那麼在 microtask 中就完成數據更新, 當前 task 結束就能夠獲得最新的 UI, 反之若是新建一個 task 來作數據更新,那麼渲染就會進行兩次。
因此性價好比此高的 MO 天然成爲了首選
關於 microtask,具體能夠閱讀 Jake 寫的 Tasks, microtasks, queues and schedules
上面關於 nextTick 的源碼實現屬於 vue 最先的版本 v1.0.9,在深挖 mutationObserver 的時候發現 nextTick 在vue的版本迭代中也在不斷的進化,同事也發生過退化,很是有趣:
先說說退化的事件,尤大(vue的做者)曾經使用 window.postMessage
來替代 MO 實現 nextTick,結果開發者使用後發現了問題,能夠看看這兩個 JSFiddle:jsfiddle1 和 jsfiddle2, 兩個例子用了不一樣版原本實現元素的絕對定位,第一個使用的是 2.0.0-rc6,這個版本採用的是 MO,然後來由於 IOS 9.3 的 WebView 裏 MO 有 bug,尤大便換成 window.postMessage來實現,即第二個實例版本爲 2.0.0-rc7, 可是因爲 postMessage 會將回調放到 macrotask 其實也就是 task 裏面,致使可能執行了屢次 UI 的task都沒有執行 window.postMessage 的 task,也就延遲了更新DOM操做的時間。尤大在後續版本撤回了這一次修改,具體的討論能夠看issue
關於進化,在後續的版本里,因爲 es6 的新語法,nextTick 開始使用 Promise.then 和 MO 來作首選和次選,在前面的討論中已經提到,Promise.then 也屬於 microtask。