先记住这个答案
Promise 构造器在执行 new Promise(executor) 时,会当场同步调用 executor,并传入 resolve 和 reject。因此同一段代码里,executor 内的 console.log 一定先于 new Promise 之后的 console.log 输出。executor 中调用 resolve 会把 Promise 标记为 resolved:传入非 thenable 值时状态同步变为 fulfilled;若传入另一个 thenable,Promise 会锁定到它,可能暂时仍处于 pending。无论哪种情况,then 回调都不会同步触发,而会被放入微任务队列,等当前同步代码执行完再运行。若 executor 同步抛出异常,Promise 会被 reject,除非此前已调用过 resolve 或 reject。
- executor 在构造期间同步调用
- resolve 不等于立即进入 fulfilled
- 回调通知异步,异常处理看首次锁定结果
构造函数何时调用 executor
Promise 构造函数在语义上是一个执行器:new Promise(executor) 被求值时,executor 会立刻以 resolveFunc 和 rejectFunc 为参数被调用一次。这个调用发生在 Promise 对象还未赋给任何变量的时刻,所以紧跟在 new Promise 之后的代码必然晚于 executor 的顶层代码运行。例如先输出 1,再 new Promise 中输出 2,再输出 3,控制台会得到 1、2、3。
executor 的返回值没有意义,Promise 实例的状态只能通过 resolve、reject 或同步 throw 改变。若 executor 调用 resolve(value) 且 value 不是 thenable,状态会同步变为 fulfilled(若 value 是 thenable,Promise 会锁定到它,可能暂时保持 pending,最终状态跟随该 thenable);但无论哪种情况,注册到该 Promise 上的 then 回调都不会在此刻运行,而是排入微任务队列。这就是 executor 同步、handler 异步的机制来源,两者分别回答“何时执行构造参数”与“何时执行观察者回调”。
console.log('start');
new Promise((resolve) => {
console.log('executor');
resolve('done');
}).then((value) => console.log('then:' + value));
console.log('end');查看输出与解释
start
executor
end
then:doneexecutor 中的日志出现在 start 与 end 之间,证明 executor 是同步调用;then 回调打印在 end 之后,说明 handler 是异步的。
包成 Promise 后,同步工作仍会阻塞当前调用
假设导入工具先解析一份较大的文本,再把解析结果交给页面展示。把解析调用放进 new Promise(resolve => resolve(parse(text))),并不会让解析自动移到后台:调用 parse(text) 和 executor 本身都发生在当前调用栈中,解析结束后构造函数才返回。返回 Promise 只改变结果的消费方式,不会自动改变计算发生在哪个线程。
判断是否能异步让出执行权,要看真正执行工作的 API。定时器可以把工作推迟到后续任务,但回调开始后仍可能阻塞主线程;需要隔离较重计算时,可以根据运行环境使用 Worker。封装已有回调接口则是另一种用途:executor 同步登记回调,等接口产生结果后调用 resolve,调用方通过 then 或 await 消费结果。
resolve 不中断调用,空 executor 永不 settle
executor 是普通函数,调用 resolve(value) 不会像 return 一样结束函数。new Promise(resolve => { resolve(1); console.log("still"); }) 中的日志仍会同步输出。传入普通值会兑现 Promise;传入 thenable 时会采纳它的后续结果,可能暂时仍是 pending。无论结果如何确定,后续观察回调都不会在 resolve 调用里同步执行。
如果执行器和已登记的回调始终不调用 resolve 或 reject,也不触发有效拒绝,这个 Promise 会一直 pending。它是否保留内存取决于可达引用及关联资源,不能只凭 pending 状态判断泄漏。外部超时竞赛可以让调用方停止等待,却不自动取消原工作或清理事件监听器;资源回收需要调用底层清理方法,并释放不再需要的引用。
容易答错的地方
- 认为 executor 是异步函数
- 若以为 executor 会等当前同步代码结束后才运行,会误判日志顺序。规范要求在 new Promise 构造时同步调用 executor。可通过在 new Promise 前后放两个 console.log 验证,executor 内的输出总在两个日志之间。
- 把 resolve 后的 then 当成同步调用
- 想当然认为先 resolve 后立刻执行 then 回调,忽略了微任务队列。正确顺序是:executor 同步跑完,then 回调在当前任务结束后执行。即使 Promise 已经是 fulfilled 状态,then 也只能异步进入微任务。
面试官还会怎么问?
executor 先调用 resolve,再同步抛错,会怎样?
例如 resolve(1) 后再同步抛错,结果仍为 1;构造器捕获异常后尝试拒绝,但首次 resolve 已锁定结果。若先 resolve(otherPromise),当前 Promise 仍会跟随 otherPromise,不保证已经 fulfilled;其后同步抛出的异常同样不会覆盖已经锁定的结果。
为什么有人误以为 executor 是异步的?
因为常见 Promise 示例把 setTimeout、fetch 等异步 API 放进 executor。看到回调风格,就误以为整个函数异步。实际上 executor 只是同步注册异步操作的起点,异步是这些 API 自身的行为。
如何快速验证 executor 是否同步?
在代码里依次放 console.log('A'); new Promise(() => console.log('B')); console.log('C');。若输出 A、B、C,则 B 位于构造期间,executor 是同步的;若输出 A、C、B,则 executor 才是异步的。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。