淺析 Vue 2.6 中的 nextTick 方法。
事件迴圈
JS 的 事件迴圈 和 任務佇列 其實是理解 nextTick 概念的關鍵。
這個網上其實有很多優質的文章做了詳細介紹,我就簡單過過了。
以下內容適用於瀏覽器端 JS,NodeJS 的事件迴圈機制並不相同。
規範中規定 task 分為兩大類: task(macrotask)
和 microtask
。
通常認為是 task
的任務源:
setTimeout / setInterval
setImmediate
MessageChannel
I/O
UI rendering
通常認為是 microtask
的任務源:
Promise
process.nextTick
MutationObserver
Object.observe(已廢棄)
簡單概況:(這裡是官方規範)
- 首先開始執行 script 指令碼,直到執行上下文棧為空時,開始清空 microtask 佇列裡的任務,佇列嘛,先入先出,出一個執行一個,清空完畢,走事件迴圈。
- 事件迴圈:不斷地去取 task 佇列的中的一個任務推入棧中執行,並在當次迴圈裡依次清空 microtask 佇列裡的任務,清空之後,可能會觸發頁面更新渲染(由瀏覽器決定)。
- 之後重複 事件迴圈 步驟。
nextTick
Vue 中資料的變化到 DOM 的更新渲染是一個非同步過程。
此方法便用於在 DOM 更新迴圈結束之後執行延遲迴調。
使用方法很簡單:
// 修改資料
vm.msg = 'Hello';
// DOM 還沒有更新
Vue.nextTick(function() {
// DOM 更新了
});
// 作為一個 Promise 使用
Vue.nextTick().then(function() {
// DOM 更新了
});
原始碼 去除註釋,其實也只有不到一百來行,整體還是很容易理解的。
這裡劃成 3 個部分介紹。
模組變數
介紹 引入的模組 和 定義的變數。
// 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 函式
nextTick 函式。接受兩個引數:
- cb 回撥函式:是要延遲執行的函式;
- ctx:指定 cb 回撥函式 的 this 指向;
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.4、2.5、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 非同步及瀏覽器更新渲染時機