MicroTask(微任务) 和 MacroTask(宏任务)
在说 nextTick
之前,需要对 microTask
, macroTask
和 Event Loop
有一定了解。详见JavaScript的运行机制Event Loop(事件循环), microTask
是把任务放在主线程的末尾,而 macroTask
是把任务放在 Task Queue
中,所以当我们执行完主线程的代码后,会先执行 microTask
,再执行 macroTask
。
常见的 microTask
有:process.nextTick(Node.js特有的)
,Promise
,MutationObserver
等;
常见的 macroTask
有: setImmediate
,MessageChannel
,setTimeout
,setInterval
等。
nextTick 的实现
在 Vue
中,nextTick
是一个被重写次数较多的一个 API。
在 2.4
之前的版本中, nextTick
的实现是一律采用 microTask
。
microTask
采用的优先级顺序为 1,Promise
; 2,MutationObserver
;3, setTimeout
。
但是
microTask
的优先级太高,会比事件冒泡还要快(#6566),以及一些顺序事件(#4521 #6690)的bug。
在 2.5
的版本中,nextTick
的实现改为 microTask
+ macroTask
的结合。默认采用 microTask
,在一些事件绑定的地方会强制使用 macroTask
。
microTask
中采用的优先级顺序为:1,Promise
; 2,macroTask
macroTask
中采用的优先级顺序为:1,setImmediate
; 2,MessageChannel
;3,setTimeout
但这也会出现一些问题,重绘问题(#6813)及一些不可修复的奇怪问题(#7109,#7153,#7546, #7834,#8109)。
所以在 2.6
的版本中,nextTick
重新改为 microTask
的实现,然后单独把 2.4
中出现的bug修复掉。但需要注意的是 2.6
中相对于 2.4
多了一个 setImmediate
。
采用的优先级顺序为: 1,Promise
; 2,MutationObserver
;3, setImmediate
;4,setTimeout
。
其中前2个为 microTask
,后两个为 macroTask
。
MutationObserver
是可以监听到指定DOM的变化;
setImmediate
是在客户端实现的process.nextTick
,现阶段只有IE实现了。
这里是相关代码
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and 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
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
接下来是 nextTick
的执行逻辑:
- 把 cb 函数先放到 callbacks 这个数组中,这样可以保证多个 cb 可以在同一个
tick
中被调用。 - 最终按照优先顺序去执行
flushCallbacks
。 - 循环执行 callbacks 中的 cb。
const callbacks = []
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]()
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 这里是保证了当前事件循环中的所有的 nextTick 的 cb ,都会在 timerFunc 中统一被调用。
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
}