事件循环机制
js中的代码,其上下文进入执行栈之后,引擎会判断是不是异步操作(如DOM事件、timer、异步请求)。如果是异步任务,浏览器会将其上下文移出执行栈,交给浏览器的其他模块(如chrome的webcore模块)去处理。等到其达到触发条件后,再将其对应的回调函数添加到 任务队列 (task queue)中去。等执行栈中的任务清空后,引擎会将任务队列中的回调函数重新压入执行栈。
微任务与宏任务
执行栈清空完毕后,引擎会将任务队列中的任务(回调函数)重新压入执行栈。这些任务分为宏任务(macrotasks)和微任务(microtasks)
- 宏任务(macrotasks): script(整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering 等对应的回调
- 微任务(microtasks): process.nextTick, Promises, Object.observe(废弃), MutationObserver 等对应的回调
在Promise/A+的规范中,Promise的实现可以是微任务,也可以是宏任务。但是大多数原生实现的Promise都是微任务。比如chrome、node。但一些polyfill中实现的Promise,其实就是宏任务。
每次事件循环,是先执行一个宏任务、然后执行所有的微任务。因为script(整体代码)也是一个宏任务,所以执行栈清空后,先取的任务是所有微任务。这是符合先宏任务、然后所有微任务的。
例:
let p = new Promise(function (resolve) {
console.log(1)
resolve()
})
p.then(function () {
console.log(2)
}).then(function () {
console.log(3)
Promise.resolve().then(function() {
console.log(4)
})
})
setTimeout(function () {
console.log(5)
},0)
其过程为:
log(1)入栈,执行完毕
第一个then、第二个then入栈,其回调被添加到微任务队列
settimeout入栈,其回调被添加到宏任务队列
执行栈清空,第一个微任务入栈并执行完毕(整体代码也相当于一个宏任务,所以开始所有微任务)
第二个微任务入栈,又插入了一个新的微任务log(4) ,第二个执行完毕
微任务log(4)入栈并执行完毕,此时所有微任务被清空
开始下一轮循环
宏任务log(5)入栈...
每次宏任务完成后,都会稍作停顿,更新UI,然后再进行下一个宏任务,以避免卡顿。所以操作dom之类的操作,放在微任务里更合适。【参考 知乎--Vue 中如何使用 MutationObserver 做批量处理?】
async 函数在实现上可以看作是promise的语法糖,
async function f() {
console.log(1)
}
// 等价于
new Promise(function(resolve, reject) {
console.log(1)
resolve();
})
所以下面这个例子:
console.log(1)
f1()
async function f1() {
console.log(2)
await f2()
console.log(3)
}
async function f2() {
console.log(4)
}
console.log(5)
// 1 2 4 5 3
相当于:
console.log(1)
new Promise(function(resolve1) {
console.log(2)
new Promise(function(resolve2) {
console.log(4)
resolve2()
}).then(res => {
console.log(3)
})
})
console.log(5)