1. 知識儲備
在閱讀源代碼之前請按順序閱讀這些文章/視頻:
Vue.js:異步更新隊列
從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014
MDN:MutationObserver
MDN:MessageChannel
Tasks, microtasks, queues and schedules
Vue.js 升級踩坑小記(可省略,但是這篇文章給我的收獲還是很大的)
2. 知識點小結(jié):
這里只做一個最簡單粗暴的知識點小結(jié),不包含任何解釋。
宏任務(macrotask)
主代碼塊,setTimeout,setInterval, setImmediate,MessageChannel,postMessage
微任務(microtask)
promise,MutationObserver
任務執(zhí)行順序以及渲染的執(zhí)行
macrotask -> microtask -> 渲染 -> macrotask -> microtask -> 渲染 -> ......
3. Vue如何實現(xiàn) .nextTick()
Vue 在內(nèi)部嘗試對異步隊列使用原生的 Promise.then 和 MessageChannel,如果執(zhí)行環(huán)境不支持,會采用 setTimeout(fn, 0) 代替。
異步隊列很明顯提高了性能,但是如果我們想要在DOM更新之后做點什么,可能就有點麻煩了(詳情看這),因為主代碼塊在微任務列表之前,而Vue是在微任務或者下一個宏任務中才更新DOM的,這時候就需要使用.nextTick()了。
Vue.js 2.5之前,幾乎都是用 microtask 來模擬 Node.js 的.nextTick():
- 瀏覽器是否支持
Promise?是則使用Promise,否則進行下一步 - 瀏覽器是否支持
MutationObserver,是則使用MutationObserver,否則進行下一步 -
setTimeout(此時是一個macrotask)
Vue.js 2.5之后,默認使用 microtask ,在DOM事件強制使用 macrotask:
- 先確定使用 macrotask 時用哪個API,優(yōu)先級為:
setImmediate->MessageChannel->setTimeout - 確定使用 microtask 時用哪個API,優(yōu)先級為:
Promise->macroTimerFunc(和macrotask一致) - 判斷是否使用 macrotask ,是則調(diào)用
macroTimerFunc,否則調(diào)用microTimerFunc - DOM事件默認會包裹一層函數(shù)來強制其使用 macrotask
4. 正題
Vue.js 2.5之前,.nextTick()放在env.js中,使用Promise, MutationObserver, setTimeout來實現(xiàn)異步隊列:
// env.js
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
// 能否使用 __proto__?
export const hasProto = '__proto__' in {}
// 瀏覽器環(huán)境檢測,和本文無關(guān),可忽略
export const inBrowser = typeof window !== 'undefined'
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE = UA && /msie|trident/.test(UA)
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isEdge = UA && UA.indexOf('edge/') > 0
export const isAndroid = UA && UA.indexOf('android') > 0
export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)
// this needs to be lazy-evaled because vue may be required before
// vue-server-renderer can set VUE_ENV
let _isServer
export const isServerRendering = () => {
if (_isServer === undefined) {
/* istanbul ignore if */
if (!inBrowser && typeof global !== 'undefined') {
// detect presence of vue-server-renderer and avoid
// Webpack shimming the process
_isServer = global['process'].env.VUE_ENV === 'server'
} else {
_isServer = false
}
}
return _isServer
}
export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
/* istanbul ignore next */
function isNative (Ctor: Function): boolean {
return /native code/.test(Ctor.toString())
}
// 相關(guān)代碼在這里
export const nextTick = (function () {
const callbacks = [] // 存放回調(diào)函數(shù)
let pending = false // 是否有異步隊列(callbacks)正在等待執(zhí)行
let timerFunc // 處理異步隊列的函數(shù)(Promise,MutationObserver,setTimeout)
function nextTickHandler () { // 清空callbacks列表,執(zhí)行callback列表中的函數(shù)
pending = false // 表示沒有異步隊列在等待了
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// nextTick利用了微任務隊列,微任務隊列可以用原生的Promise或者MutationObserver來實現(xiàn)
// MutationObserver被廣泛支持,但是在iOS >= 9.3.3上會有嚴重的bug。
// 因此優(yōu)先使用Promise
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) { // 用Promise把回調(diào)函數(shù)推入微任務隊列
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// 在UIWebViews中雖然Promise.then沒有完全break,但是會陷入一個很奇怪的狀態(tài)
//回調(diào)函數(shù)都被推入微任務隊列中,但是在瀏覽器處理別的任務(比如timer)之前隊列不會被清空。
// 因此添加一個空的timer來強制清空微任務隊列。
if (isIOS) setTimeout(noop)
}
} else if (typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Promise不能用則用MutationObserver,MutationObserver也屬于微任務
// e.g. PhantomJS IE11, iOS7, Android 4.4
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter)) // 創(chuàng)建一個看不見的文本節(jié)點,讓MutationObserver來監(jiān)聽它
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else { // Promise和MutationObserver都不能用
// 用setTimeout代替,setTimeout為宏任務
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (cb?: Function, ctx?: Object) { // 添加回調(diào)函數(shù),調(diào)用VUe.nextTick即調(diào)用這個函數(shù),注意到可以傳入一個對象做為該函數(shù)的上下文!
let _resolve
callbacks.push(() => { // 包裹傳入的函數(shù),綁定其上下文,并push到callbacks中
if (cb) cb.call(ctx)
if (_resolve) _resolve(ctx)
})
if (!pending) { // 如果沒有異步隊列在等待執(zhí)行,那么處理當前的異步隊列
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
})()
let _Set
/* istanbul ignore if */
if (typeof Set !== 'undefined' && isNative(Set)) { // 瀏覽器支持Set
// use native Set when available.
_Set = Set
} else { // 瀏覽器不支持Set
// a non-standard Set polyfill that only works with primitive keys.
_Set = class Set {
set: Object;
constructor () {
this.set = Object.create(null)
}
has (key: string | number) { // set[key]是否存在
return this.set[key] === true
}
add (key: string | number) { // 添加一個元素
this.set[key] = true
}
clear () { // 清空對象內(nèi)所有元素
this.set = Object.create(null)
}
}
}
export { _Set }
?
Vue.js 2.5+把nestTick單獨成一個文件了:next-tick.js:
// next-tick.js
/* @flow */
/* globals MessageChannel */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'
const callbacks = [] // 存儲回調(diào)函數(shù)
let pending = false // 當前是否有異步隊列在等待執(zhí)行?
function flushCallbacks () { // 執(zhí)行任務隊列中的回調(diào)函數(shù)
pending = false // pending為false,表示異步隊列已經(jīng)被清空
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 在<2.4 的版本,nextTick幾乎都是使用microtasks來實現(xiàn)
// 但這會導致一些問題(下面講)
// 所以 2.5+ 默認使用microtasks,但某些場景下會強制使用 macrotasks(比如,v-on綁定的事件)
let microTimerFunc
let macroTimerFunc
let useMacroTask = false // 是否使用macrotask來處理nextTick?默認為否
// 決定macrotask的實現(xiàn)。
// 在技術(shù)上 setImmediate 是最理想的,但是它只能在IE中使用。
// 讓回調(diào)函數(shù)始終排隊在 同一個事件循環(huán)中觸發(fā)的DOM事件 之后的唯一polyfill就是使用MessageChannel
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // 優(yōu)先使用 setImmediate
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else { // 不支持 setImmediate 和 MessageChannel 時用 setTimeout 代替
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 決定MicroTask的實現(xiàn)。
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
// 在UIWebViews中雖然Promise.then沒有完全break,但是會陷入一個很奇怪的狀態(tài)
// 回調(diào)函數(shù)都被推入微任務隊列中,但是在瀏覽器處理別的任務(比如timer)之前不會執(zhí)行這些任務
// 因此添加一個空的timer來強制清空微任務隊列。
if (isIOS) setTimeout(noop)
}
} else {
// 不支持Promise則用macrotask代替
// MutationObeserver因為兼容性問題被拋棄了
microTimerFunc = macroTimerFunc
}
// 包裹一個函數(shù),強制其使用macrotask
// 默認會給每一個DOM事件的回調(diào)函數(shù)調(diào)用withMacroTask
export function withMacroTask (fn: Function): Function {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true // 使用macrotask
const res = fn.apply(null, arguments)
useMacroTask = false // 狀態(tài)重新設(shè)置為false,不然其他回調(diào)函數(shù)也會用macrotask
return res
})
}
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()
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
5. 為什么默認使用 microtask 的一點補充
(摘自知乎,原鏈接:Vue 中如何使用 MutationObserver 做批量處理?)
根據(jù)HTML Standard,在每個 task 運行完以后,UI 都會重渲染(知識點小結(jié)那塊有說到任務執(zhí)行順序以及什么時候渲染),那么在 microtask 中就完成數(shù)據(jù)更新,當前 task 結(jié)束就可以得到最新的 UI 了。反之如果新建一個 task 來做數(shù)據(jù)更新,那么渲染就會進行兩次。(當然,瀏覽器實現(xiàn)有不少不一致的地方,上面 Jake 那篇文章里已經(jīng)有提到。)