先记住这个答案
因为规范规定 then 的回调必须放进微任务队列,等当前执行的同步代码全部结束后才调用,哪怕 Promise 在调用 then 之前就已经兑现。这样做是为了保证一致性:同一个 API 的回调,不能有时同步执行、有时异步执行,否则调用方后面的代码行为会跟着数据状态变来变去。微任务的执行时机又足够早,在当前同步代码结束后、下一个宏任务之前,所以不会带来明显延迟。
then回调一律进微任务队列,从不同步执行- 已兑现的 Promise 也要等同步代码跑完
- 目的是避免「有时同步有时异步」的回调
- 微任务在下一个宏任务之前执行,比
setTimeout早
已经兑现的 Promise 也要排队
先看一个最小的例子,p 在调用 then 之前就已经是 fulfilled 了。
const p = Promise.resolve(1) // 已经兑现
console.log('A')
p.then(() => console.log('C'))
console.log('B')查看输出与解释
A
B
C调用 p.then 的时候,结果明明已经在手上了,引擎也不会当场执行回调,而是把它放进微任务队列。等 console.log('B') 执行完、调用栈清空,才从队列里取出来执行。
微任务可以理解成「当前这件事做完后马上要做的小事」。浏览器每处理完一个宏任务(一段脚本、一次点击回调、一个定时器回调),都会先把微任务队列清空,再去做渲染和下一个宏任务。
为什么不允许同步执行
问题出在「有时同步有时异步」的回调上,社区管这种情况叫 Zalgo。下面是一个带缓存的回调风格函数,缓存命中时同步回调,没命中时异步回调。
const cache = new Map()
function getUser(id, cb) {
if (cache.has(id)) return cb(cache.get(id)) // 命中缓存:同步回调
setTimeout(() => { cache.set(id, 'Tom'); cb('Tom') }, 10) // 未命中:异步回调
}
function load(label) {
let loading = true
getUser(1, () => console.log(label, 'loading =', loading))
loading = false
}
load('第一次')
setTimeout(() => load('第二次'), 20)查看输出与解释
第一次 loading = false
第二次 loading = true同样的 load 函数,调用两次,回调里看到的 loading 却不一样。第一次缓存没命中,回调在 loading = false 之后才执行;第二次命中缓存,回调同步执行,loading 还是 true。真实项目里这种 bug 会随数据状态时有时无,很难复现。
Promise 把回调统一变成异步,就彻底消除了这类问题。不管数据是现成的还是要等网络,then 后面的同步代码一定先执行完,写代码的人只需要记一种情况。
- 为什么选微任务而不是
setTimeout setTimeout是宏任务,要排在其它定时器、事件回调后面,中间还可能插入一次渲染,而且嵌套多层后浏览器会把延迟至少拉到 4 毫秒。用微任务既能保证异步,又能在当前任务结束后立刻执行,一长串then链不会被拖慢。
容易答错的地方
- 以为 Promise 里的代码也是异步的
new Promise(executor)里的 executor 是同步执行的,传进去立刻就跑。异步的只是then、catch、finally的回调。「Promise 是异步的」这种笼统说法在面试里会被追问。- 说
then回调是宏任务 then回调是微任务。区分它和setTimeout的关键就在这里:同一轮里,所有微任务都会在下一个setTimeout回调之前执行完。- 以为
resolve时回调马上执行 resolve只是把已经登记的回调放进微任务队列,然后立刻返回。resolve后面的同步代码会先执行,回调要等调用栈清空才跑。
面试官还会怎么问?
await 后面的代码是同步还是异步?
异步。await 相当于把后面的代码包进一个 then 回调,哪怕 await 的是普通值,比如 await 1,后面的代码也要等一个微任务才继续执行。
微任务里再产生微任务会怎样?
新产生的微任务会排进同一个队列,在本轮继续执行,直到队列为空才进入下一个宏任务。所以如果写了一个无限递归的 then 链或 queueMicrotask,页面会完全卡死,定时器和渲染都没有机会执行。
除了 Promise,还有哪些东西会产生微任务?
queueMicrotask 可以直接往队列里放一个微任务,MutationObserver 的回调也是微任务。在 Node.js 里还有 process.nextTick,它的优先级比 Promise 的微任务还高。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。