写过 JS 的人都踩过这个坑——Promise 每次都要在外面声明 resolve 和 reject,今天 ES2024 新语法把这件事彻底原生化了

写过这种代码吗?Promise 的 resolve 和 reject 总要先在外层声明,然后在构造函数里赋值:

// 旧写法:外面先声明,里面再赋值
let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

这串代码又丑又容易出 bug——resolve 和 reject 跟 promise 的生命周期完全是脱节的。而且这种模式在前端代码里极其常见:WebSocket 通信、事件驱动的 API 封装、请求/响应配对……几乎每个写异步代码的前端工程师都踩过这个坑。

现在 ES2024Promise.withResolvers() 一行搞定:

const { promise, resolve, reject } = Promise.withResolvers();
someEvent.on('done', resolve);

这就是今天的升级。Chrome 119+/Firefox 121+/Safari 17.4+/Node 22+,约 95% 全球覆盖,Baseline Widely Available。

原理

Promise.withResolvers() 返回一个普通对象:{ promise, resolve, reject }。它本质上等价于在外层声明 resolve/reject 再在 Promise 构造函数的 executor 里赋值,但更简洁,且不存在外层变量泄漏。

实战场景:WebSocket 请求/响应配对

这是最典型的用法。WebSocket 发请求后不知道哪个响应对哪个请求,得靠 correlation id 配对:

class WebSocketClient {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.pending = new Map();
    this.ws.onmessage = (event) => {
      const { id, data, error } = JSON.parse(event.data);
      const resolvers = this.pending.get(id);
      if (resolvers) {
        this.pending.delete(id);
        error ? resolvers.reject(new Error(error)) : resolvers.resolve(data);
      }
    };
  }

  sendRequest(payload) {
    const id = crypto.randomUUID();
    const { promise, resolve, reject } = Promise.withResolvers();
    this.pending.set(id, { resolve, reject });
    this.ws.send(JSON.stringify({ id, ...payload }));
    setTimeout(() => {
      if (this.pending.has(id)) {
        this.pending.delete(id);
        reject(new Error('Request timeout'));
      }
    }, 10000);
    return promise;
  }
}

不用 Promise.withResolvers() 的话,resolve 和 reject 就要先在外层 let 声明,代码结构更散、更容易出错。

实战场景:Stream 转异步迭代器

MDN 官方示例,每次读取一个批次后重新创建新 promise:

async function* readableToAsyncIterable(stream) {
  let { promise, resolve, reject } = Promise.withResolvers();
  stream.on('error', reject);
  stream.on('end', resolve);
  stream.on('readable', () => resolve());

  while (stream.readable) {
    await promise;
    let chunk;
    while ((chunk = stream.read())) {
      yield chunk;
    }
    ({ promise, resolve, reject } = Promise.withResolvers());
  }
}

四个反模式要避开

  1. resolve/reject 不要嵌套超过一层:如果 resolver 离开了文件创建时的作用域,用 async 函数包一层更安全。
  2. 不要在 React 组件 render 里调用:状态管理用 Zustand/Redux Toolkit/Jotai/Pinia 的异步 action,数据获取走 TanStack Query/SWR。
  3. 每个路径都要 resolve 或 reject 一次:否则 promise 和两个闭包会永久驻留内存,长时间运行的 Node 服务器会有真实泄漏风险。
  4. 不要用来构建通用事件发射器:用 EventTarget、Node EventEmitter 或 async iterator。

浏览器支持

Node.js 22+/Bun 1.1+/Deno 1.40+,所有现代浏览器(Chrome 128+、Safari 18+、Firefox 128+),以及 Vercel/Cloudflare Workers/Deno Deploy/Netlify Functions/AWS Lambda/Fastly Compute@Edge 全平台支持。Legacy 浏览器用 core-js polyfill。

如果你的代码库里还有在外层声明 resolve/reject 的旧模式,现在可以用一行替换:代码更少,作用域更干净,bug 更少。ES2024 之后,不用再忍那串样板代码了。

评论区

0 条评论

登录后可评论。

阿柯·前端架构 11 阅读