代码位置
nextTick
的实现在src/core/util/next-tick.js
中。
主要代码块
- 根据当前环境,选择实现
nextTick
异步回调的途径。
// 首先是看当前环境支不支持Promise,如果支持Promise就使用Promise,添加了一个微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) { // 如果不支持Promise,就看支持不支持MutationObserver,相当于添加了一个微任务
// e.g. PhantomJS, iOS7, Android 4.4
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)) { // 再看支不支持setImmediate,添加了一个宏任务,在下一轮的事件队列中调用执行
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else { // 都不支持就使用setTimeout来代替,添加了一个宏任务,在下一轮的事件队列中调用执行
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve;
callbacks.push(() => {
// 判断nextTick中有没有传入回调,传入了回调就把回调放到callbacks的回调队列中
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) { // 没有传入回调,就使用默认的回调,前提是默认的回调存在
_resolve(ctx)
}
})
if (!pending) { // 当回调队列没有执行时,才会去触发新的执行操作
pending = true // 表示异步队列正在执行
timerFunc() // 调用异步执行的方法,就是上面那一步的操作
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') { // 当没有传入回调,并且Promise可以使用时,把默认的回调变成Promise的resolve操作,也就是支持nextTick().then()的方式调用
return new Promise(resolve => {
_resolve = resolve
})
}
}
function flushCallbacks () {
pending = false // 清空队列之后,当前的状态又是pending等待中
const copies = callbacks.slice(0) // 将回调队列拷贝一份出来
callbacks.length = 0 // 清空回调队列
for (let i = 0; i < copies.length; i++) {
copies[i]() // 以此调用回调队列的方法
}
}
原理解读
nextTick
的本质是利用了JS
的事件队列的机制,动态的添加微任务或者宏任务,使得在nextTick
中的回调可以异步调用,通常是在页面的data
更新过之后。
事件队列
- JS存在一个主线程,专门用来执行所有的同步任务,形成了一个执行栈。
- 主线程之外,存在一个任务队列,异步任务的回调就放在任务队列中。
- 一旦当前执行栈中的同步任务全部执行完毕,系统就会去依次读取任务队列中的回调,放到主线程上去执行。
- 进入下一个事件循环,重复以上三步。
事件队列中的常见宏任务和微任务
- 宏任务
- I/O
- setTimeout,将事件插入到了事件队列,直到过了设置的时间之后才可以执行,不一定是刚好那个时间,而且必须要等待当前的执行栈执行完毕才会去调用,即使时间间隔为0。(浏览器默认还是会给一个1ms的延迟)
- setInterval,与setTimeout类似。
- setImmediate,如果不发生任何I/O操作,那么它会比setTimeout(fn, 0)要快,与setTimeout(fn, 0)功能类似,两个方法不能确定谁先执行。但是当主线程运行的时间比较长(大于setTimeout的延迟时间)的时候,往往setTimeout(fn, 0)要比setImmediate先执行。
- requestAnimationFrame,间隔时间是根据浏览器的刷新频率决定的,与setTimeout也有点类似。
- 微任务
- process.nextTick,将事件插入到主线程的末尾,也就是所有微任务和宏任务之前。
- MutationObserver,当监听的dom发生变化时,加入微任务队列。
- Promise.then(catch,finally),直接加入微任务队列。
- 执行顺序的问题:
- 一次事件循环中,一定要把微任务执行完毕之后才会去执行宏任务,包括微任务中动态添加的微任务。
- 看下面的代码。
// 当只有三个宏任务的时候,结果是123/132/213,都有可能
setTimeout(() => {
console.log(1)
}, 0)
setImmediate(() => {
console.log(2)
})
setTimeout(() => {
console.log(3)
}, 0)
- 再看另一种情况
// 当主线程存在其它任务时,基本上就是setImmediate要比setTimeout(fn, 0)要执行的晚一点,所以尝试了很多次还是48756132
setTimeout(() => {
console.log(1) // 宏任务第一步
}, 0)
setImmediate(() => {
console.log(2) // 宏任务第三步
})
setTimeout(() => {
console.log(3) // 宏任务第二步
}, 0)
new Promise((resolve) => {
console.log(4); // 主线程第一步
resolve();
}).then(() => {
console.log(5); // 微任务队列第二步
}).finally(() => {
console.log(6) // 微任务队列第三步
});
process.nextTick(() => {
console.log(7) // 主线程末尾执行,微任务第一步
});
console.log(8); // 主线程第二步
- 最后看一种情况
// 由于必须要先把此次事件队列中的微任务执行完毕后才可以去执行宏任务
// 又因为在微任务中定义的微任务依然需要在此次事件循环中执行完
// 微任务或宏任务中定义的宏任务会放到下一次的事件循环中去执行
// 所以最终结果就是:1 > 4 > 3 > 5 > 7 > 6 > 2 > 8
process.nextTick(() => {
console.log(1); // 微任务第一步
setTimeout(() => {
console.log(2); // 下一轮的宏任务第一步
}, 0)
Promise.resolve(3).then((data) => {
console.log(data) // 微任务第三步
});
})
Promise.resolve(4).then(data => {
console.log(data); // 微任务第二步
})
setTimeout(() => {
console.log(5); // 宏任务第一步
Promise.resolve(6).then(data => {
console.log(data); // 下一轮的微任务第二步
})
process.nextTick(() => {
console.log(7); // 下一轮的微任务第一步
})
setTimeout(() => {
console.log(8); // 下一轮的宏任务第二步
})
})