介紹 Vue 異步更新前,要先了解一下 JS 運行時如何處理程式碼的異步執行
js 執行時是單執行緒,它基於事件循環達到異步執行。
我們用下面範例來看一下,一個事件循環(Event Loop)的週期
左邊是我們要執行的程式碼
圖片來源:What the heck is the event loop anyway? | Philip Roberts | JSConf EU
可以看到 stack 開始運行程式碼console.log('Hi')
console.log('Hi')
執行完會從 stack 消失,接著執行 setTimeout
setTimeout
是異步任務,要等待運行結果
setTimeout
還再等待運行結果,但對 stack 來說需要繼續執行console.log('JSConfEU')
當程式碼都執行完,stack 也清空了
setTimeout
運行完就把 cb function 推到任務隊列(task queue)裡
事件循環()的工作就是當 stack 清空了,就把在任務隊列(task queue)裡的 call back 推到 stack 裡繼續執行 console.log('there')
其實異步任務分兩種
異步更新可以避免過度渲染,以下面範例來看
// Task 1
this.content = '第一次'
// Task 2
this.content = '第二次'
// Task 3
this.content = '第三次'
觀察數據的多次變化,我們對同一個狀態修改了三次,如果採取同步更新,同步調用 watcher.run 就會觸發 3 次 DOM 操作,但其實只需要把第3次的內容渲染出就好。
所以 Vue 採取異步執行,推入 task queue 中等待下一個 Event Loop 再操作 task queue 中的 Watcher,因為是同一個 Watcher,只會調用一次 watcher.run,達到只觸發 1 次 DOM 操作。
nextTick 是 Vue 用來把更新狀態的操作包裝成異步操作派發出去
// src/core/util/next-tick.js
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nextTick
把傳入的callback function cb
推到 callbacks
陣列中,最後根據 useMacroTask
條件判斷要執行 macroTimerFunc
或 microTimerFunc
派發方式
macroTimerFunc
和 microTimerFunc
實現如下
// packages/weex-vue-framework/factory.js
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = function () {
setImmediate(flushCallbacks);
};
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = function () {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// packages/weex-vue-framework/factory.js
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
microTimerFunc = function () {
p.then(flushCallbacks);
if (isIOS) { setTimeout(noop); }
};
} else {
// fallback to macro
microTimerFunc = macroTimerFunc;
}
不管是派發 macroTimerFunc
或 microTimerFunc
,派發的任務對象都是 flushCallbacks
// src/core/util/next-tick.js
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
flushCallbacks
處理幾件事
callbacks
陣列中。callbacks
陣列是實現任務隊列(task queue)的形式callbacks
陣列)再推進 micro 或 macro 隊列前,會先去檢查當前是否有異步任務正在執行(即檢查 pending
)pending
是用來保證更新任務的有序進行,避免發生混亂callbacks
陣列)的任務進行派發(推進 micro 或 macro 隊列)和執行任務隊列(task queue)分兩種?
寫錯惹!是異步任務分為 macro task 和 micro task 才對