先记住这个答案
executor 是同步执行的。new Promise(fn) 执行到这一行时,fn 会被立即调用,里面的代码和外面的同步代码按书写顺序依次运行。在 executor 里调用 resolve(),也只是把 Promise 的状态同步改成 fulfilled,并不会马上执行 then 的回调。then 回调会被放进微任务队列,等当前这段同步代码全部跑完才执行。所以一句话记:构造同步,通知异步。
new Promise(fn)时fn立即同步执行resolve()只改状态,不会马上执行then回调then回调一定异步,哪怕 Promise 早就成功了- executor 里同步抛错,Promise 直接变成 rejected
先看一道打印顺序题
面试里这个知识点几乎都以打印顺序题的形式出现。先自己猜一下输出,再看结果。
then 谁先打印JavaScriptconsole.log('start')
const p = new Promise((resolve) => {
console.log('executor') // new 的时候就执行
resolve('done') // 状态马上变成 fulfilled
console.log('after resolve') // resolve 不会中断 executor,这行照样打印
})
p.then((value) => console.log('then:', value)) // 回调进微任务队列
console.log('end')查看输出与解释
start
executor
after resolve
end
then: done前四行都是同步打印的,顺序和代码书写顺序一致。then 的回调虽然注册时 Promise 已经成功了,也不会立刻执行,而是排进微任务队列,等最后一行 console.log('end') 跑完才轮到它。
还有一个细节:resolve('done') 之后的 after resolve 依然打印了。resolve 只是改状态,不是 return,不会跳出 executor。如果想让后面的代码不执行,要写成 return resolve('done')。
为什么要这样设计
executor 同步执行,是为了让你能在构造的那一刻就把异步操作发起。比如把 setTimeout 包成 Promise,写在 executor 里的 setTimeout 调用会在 new 时立刻注册定时器,不会因为「还没人调用 then」而拖延。Promise 从创建起就已经在干活了,这也是它被叫做「急切求值」的原因:不管有没有人 then,里面的操作都会执行。
then 回调必须异步,则是为了顺序可预测。假设 Promise 已经成功时 then 回调同步执行、还没成功时异步执行,同一段代码的执行顺序就要看运气。规范规定回调一律异步,你写在 then 后面的同步代码永远先于回调执行,这样就不用去猜。
executor 里的异步代码和同步错误
executor 本身同步,但它包裹的操作可以是异步的,这两件事要分开看。
setTimeoutJavaScriptconst p = new Promise((resolve) => {
console.log('1 executor 开始')
setTimeout(() => {
console.log('3 定时器触发')
resolve('ok') // 这时状态才变成 fulfilled
}, 100)
console.log('2 executor 结束,Promise 还是 pending')
})
p.then((v) => console.log('4 then:', v))查看输出与解释
1 executor 开始
2 executor 结束,Promise 还是 pending
3 定时器触发
4 then: okexecutor 同步跑完时,定时器还没触发,Promise 处于 pending(等待中)。100 毫秒后回调执行 resolve('ok'),状态才确定,then 回调随后作为微任务执行。
- executor 里同步抛错
- executor 执行时如果同步
throw了一个错误,Promise 构造器会把它捕获,Promise 直接变成 rejected,错误可以用catch接住,外面的代码不会被打断。但如果错误是在 executor 里的setTimeout回调中抛的,那时 executor 早就执行完了,构造器已经管不到,这个错误会变成一个普通的未捕获异常,Promise 一直停在 pending。
容易答错的地方
- 以为 Promise 里的代码都是异步的
- 很多人看到 Promise 就默认「里面的都异步」,于是把
executor的打印排在end后面。实际上 executor 和普通函数调用没区别,异步的只有then、catch、finally这些回调。 - 以为调用
resolve就会立刻执行then回调 resolve只负责改状态。回调执行的时机永远是「当前同步代码结束之后」,所以哪怕 Promise 一创建就成功了,then回调也要排队。- 以为
resolve之后的代码不会执行 resolve不是return。它后面的语句会继续跑,只是再调用resolve或reject已经无效,因为状态只能改一次。
面试官还会怎么问?
如果 then 回调里再 new Promise,里面的 executor 什么时候执行?
在那个 then 回调执行的时候同步执行。executor 永远在 new 的那一刻运行,只不过这次 new 本身发生在一个微任务里。
连续调用两次 resolve 或者先 resolve 再 reject 会怎样?
只有第一次有效。Promise 状态从 pending 改成 fulfilled 或 rejected 之后就锁定了,后面的调用会被静默忽略,不报错也不生效。
async 函数里 await 之前的代码是同步的吗?
是的。调用 async 函数时,函数体会同步执行到第一个 await 为止,这和 executor 同步执行是一个道理。await 之后的代码才会变成微任务,在当前同步代码结束后继续。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。