淺析 Vue 2.6 中的 nextTick 方法。html
JS 的 事件循環 和 任務隊列 實際上是理解 nextTick 概念的關鍵。
這個網上其實有不少優質的文章作了詳細介紹,我就簡單過過了。vue
如下內容適用於瀏覽器端 JS,NodeJS 的事件循環機制並不相同。java
規範中規定 task 分爲兩大類: task(macrotask)
和 microtask
。git
一般認爲是 task
的任務源:github
setTimeout / setInterval setImmediate MessageChannel I/O UI rendering
一般認爲是 microtask
的任務源:web
Promise process.nextTick MutationObserver Object.observe(已廢棄)
簡單概況:(這裏是官方規範)api
Vue 中數據的變化到 DOM 的更新渲染是一個異步過程。
此方法便用於在 DOM 更新循環結束以後執行延遲迴調。
使用方法很簡單:數組
// 修改數據 vm.msg = 'Hello'; // DOM 尚未更新 Vue.nextTick(function() { // DOM 更新了 }); // 做爲一個 Promise 使用 Vue.nextTick().then(function() { // DOM 更新了 });
源碼 去除註釋,其實也只有不到一百來行,總體仍是很容易理解的。瀏覽器
這裏劃成 3 個部分介紹。bash
介紹 引入的模塊 和 定義的變量。
// noop 空函數,可用做函數佔位符 import { noop } from 'shared/util'; // Vue 內部的錯誤處理函數 import { handleError } from './error'; // 判斷是IE/IOS/內置函數 import { isIE, isIOS, isNative } from './env'; // 使用 MicroTask 的標識符 export let isUsingMicroTask = false; // 以數組形式存儲執行的函數 const callbacks = []; // nextTick 執行狀態 let pending = false; // 遍歷函數數組執行每一項函數 function flushCallbacks() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } }
接下來是核心的 異步延遲函數。這裏不一樣的 Vue 版本採用的策略其實並不相同。
2.6 版本優先使用 microtask 做爲異步延遲包裝器。
2.5 版本則是 macrotask 結合 microtask。然而,在重繪以前狀態改變時會有小問題(如 #6813)。此外,在事件處理程序中使用 macrotask 會致使一些沒法規避的奇怪行爲(如#7109,#7153,#7546,#7834,#8109)。
因此 2.6 版本如今又改用 microtask 了,爲何是又呢。。由於 2.4 版本及以前也是用的 microtask。。。
microtask 在某些狀況下也是會有問題的,由於 microtask 優先級比較高,事件會在順序事件(如#4521,#6690 有變通方法)之間甚至在同一事件的冒泡過程當中觸發(#6566)。
// 核心的異步延遲函數,用於異步延遲調用 flushCallbacks 函數 let timerFunc; // timerFunc 優先使用原生 Promise // 本來 MutationObserver 支持更廣,但在 iOS >= 9.3.3 的 UIWebView 中,觸摸事件處理程序中觸發會產生嚴重錯誤 if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks); // IOS 的 UIWebView,Promise.then 回調被推入 microtask 隊列可是隊列可能不會如期執行。 // 所以,添加一個空計時器「強制」執行 microtask 隊列。 if (isIOS) setTimeout(noop); }; isUsingMicroTask = true; // 當原生 Promise 不可用時,timerFunc 使用原生 MutationObserver // 如 PhantomJS,iOS7,Android 4.4 // issue #6466 MutationObserver 在 IE11 並不可靠,因此這裏排除了 IE } else if ( !isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || // PhantomJS 和 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; // 若是原生 setImmediate 可用,timerFunc 使用原生 setImmediate } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks); }; } else { // 最後的倔強,timerFunc 使用 setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0); }; }
一句話總結優先級:microtask 優先。
Promise > MutationObserver > setImmediate > setTimeout
nextTick 函數。接受兩個參數:
Vue 實例方法 $nextTick 作了進一步封裝,把 ctx 設置爲當前 Vue 實例。
export function nextTick(cb?: Function, ctx?: Object) { let _resolve; // cb 回調函數會經統一處理壓入 callbacks 數組 callbacks.push(() => { if (cb) { // 給 cb 回調函數執行加上了 try-catch 錯誤處理 try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); // 執行異步延遲函數 timerFunc if (!pending) { pending = true; timerFunc(); } // 當 nextTick 沒有傳入函數參數的時候,返回一個 Promise 化的調用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve; }); } }
總體看過來,感受仍是比較好理解的吧~ 2.6 版本相比以前簡化了一點。
小結一下,每次調用 Vue.nextTick(cb)
會作些什麼:
cb 函數經處理壓入 callbacks 數組,執行 timerFunc 函數,延遲調用 flushCallbacks 函數,遍歷執行 callbacks 數組中的全部函數。
延遲調用優先級以下:
Promise > MutationObserver > setImmediate > setTimeout
其實 Vue 2.四、2.五、2.6 版本的 nextTick 策略都略不同。
總體 2.6 和 2.4 的比較類似。(仔細瞅了瞅,基本就是同樣的,2.6 timerFunc 多了個 setImmediate 判斷)
2.5 版本其實也差很少。。。源碼寫法有些不同,總體優先級是:Promise > setImmediate > MessageChannel > setTimeout,若是更新是在 v-on 事件處理程序中觸發的,nextTick 會優先使用 macrotask。感興趣可自行去末尾的參考地址進行查閱。
參考:
API—Vue.js
事件循環規範
Vue nextTick v2.6.7 源碼
Vue nextTick v2.5.22 源碼
Vue nextTick v2.4.4 源碼
從 event loop 規範探究 javaScript 異步及瀏覽器更新渲染時機