每位 JavaScript 开发者都至少写过一次这样的代码:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// 稍后…
eventBus.on('done', () => resolve(result));
Enter fullscreen mode Exit fullscreen mode
你需要从构造函数外部触发 Promise——通过事件监听器、回调或消息处理器。因此你把 resolve 和 reject 泄露到外部作用域来捕获它们。它能工作,但每次写的时候都感觉不对劲,因为你正在手动逃离一个本不该被逃离的闭包。
ES2024 为此添加了正确的工具:Promise.withResolvers。
Promise.withResolvers 返回什么
与其构造一个 Promise 并逃离它的回调,不如调用一个静态方法,一次性获得全部三部分:
const { promise, resolve, reject } = Promise.withResolvers();
Enter fullscreen mode Exit fullscreen mode
promise 是一个普通的 Promise。resolve 和 reject 是它的解析函数,已经存在于你的作用域中。不需要逃离闭包。以前需要四行的模式现在只需一行。
旧模式和新模式的行为完全相同——相同的微任务时机、相同的错误传播、相同的 .then/.catch 接口。不同之处在于现在意图是明确的:你是有意创建一个延迟的 Promise,而不是作为一种变通方法。
事件到 Promise 的桥梁
最清晰的用例是将基于事件的 API 包装成 Promise 接口。之前:
function waitForOpen(socket) {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
socket.addEventListener('open', () => resolve());
socket.addEventListener('error', (e) => reject(e));
return promise;
}
Enter fullscreen mode Exit fullscreen mode
之后:
function waitForOpen(socket) {
const { promise, resolve, reject } = Promise.withResolvers();
socket.addEventListener('open', () => resolve());
socket.addEventListener('error', (e) => reject(e));
return promise;
}
Enter fullscreen mode Exit fullscreen mode
相同的逻辑,更少的仪式。该函数返回一个 Promise,根据 socket 的事件来解析或拒绝——这正是你从干净版本中读到的内容。旧版本迫使你绕过逃离样板代码才能理解。
异步队列与消息传递
延迟 Promise 反复出现的地方是异步队列——消费者等待下一个项目,生产者在稍后调用 push 的结构:
function createQueue() {
const pending = [];
let deferred = Promise.withResolvers();
return {
push(item) {
pending.push(item);
deferred.resolve();
deferred = Promise.withResolvers();
},
async *[Symbol.asyncIterator]() {
while (true) {
await deferred.promise;
while (pending.length) {
yield pending.shift();
}
}
},
};
}
const queue = createQueue();
// 消费者
(async () => {
for await (const item of queue) {
console.log('received:', item);
}
})();
// 生产者(其他地方)
queue.push('hello');
queue.push('world');
Enter fullscreen mode Exit fullscreen mode
每次调用 Promise.withResolvers() 都会创建一个新的门。消费者等待它;生产者在有内容可读时解析它。没有 withResolvers,这些门中的每一个都需要逃离样板代码。有了它,队列逻辑可以直接读懂。
受控刷新:等待一批数据
另一种模式:收集项目以进行批处理操作,并在批处理触发时一次性解析所有等待者。
class Batcher {
#items = [];
#deferred = Promise.withResolvers();
add(item) {
this.#items.push(item);
return this.#deferred.promise;
}
async flush() {
const batch = this.#items.splice(0);
const { resolve } = this.#deferred;
this.#deferred = Promise.withResolvers();
const results = await this.#processBatch(batch);
resolve(results);
return results;
}
}
Enter fullscreen mode Exit fullscreen mode
调用者通过 await batcher.add(item) 获取结果,当批处理刷新时(无论是通过计时器、尺寸限制还是显式用户操作触发)。每次刷新都通过一次 Promise.withResolvers() 调用干净地重置门。
何时不该使用它
延迟 Promise 是针对特定情况的工具,而不是 Promise 构造函数形式的通用替代品。如果你同时控制设置和解析——fetch、fs.readFile,任何你直接 await 的异步 API——请直接使用 async/await 或 Promise 构造函数。延迟模式专门用于设置和解析发生在不同的执行上下文中,无法干净地共享回调的情况。
一个具体的反模式:如果你发现自己在创建延迟的同一个函数中同步调用 resolve,你可能只是想要 new Promise(res => res(value))——也就是 Promise.resolve(value)。
TypeScript 支持
TypeScript 在 5.4 版本中添加了 Promise.withResolvers。返回类型被正确地类型化——PromiseWithResolvers<T> 会保留类型:
const { promise, resolve, reject } = Promise.withResolvers<string>();
resolve('hello'); // ✅ string
resolve(42); // ❌ type error
Enter fullscreen mode Exit fullscreen mode
如果你使用的是较旧的 TS 目标,请在 tsconfig.json 的 lib 中添加 "ES2024"。
浏览器支持
Promise.withResolvers 是 Baseline 2024:Chrome 119、Firefox 121、Safari 17.4、Node.js 22。如果你面向的是现代环境,无需 polyfill 即可使用。对于较旧的目标,旧的逃离模式仍然正确——withResolvers 是你可以始终手动编写的语法糖。
总结
在代码库中搜索 let resolve 或 let reject 后跟 new Promise 的模式。这些都是以困难方式编写的延迟 Promise。Promise.withResolvers() 为该模式命名,消除了逃离样板代码,并使意图一目了然。结果相同;代码明显更简洁。
感谢阅读!让我们保持联系:
- ⭐ GitHub — 关注我并为项目点星:github.com/parsajiravand
- 📸 Instagram — 前端最佳实践,每天更新:@bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email(工作和合同咨询):[email protected]
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.