Promise.withResolvers的使用与兼容方法

1,769 阅读5分钟

本文围绕 ECMAScript 2024 的一个新特性 Promise.withResolvers,从基础介绍、使用场景、兼容处理方案三个方面展开探讨。

Promise.withResolvers是什么?

MDN 文档对于 Promise.withResolvers 的介绍是这样的,

Promise.withResolvers()  静态方法返回一个对象,其包含一个新的 Promise 对象和两个函数,用于解决或拒绝它,对应于传入给 Promise() 构造函数执行器的两个参数。

也就是说,它创造了一个新的 Promise 实例以及与之关联的 resolvereject 方法。

它的出现,引入了创建 Promise 实例的新方式,在此之前我们创建 Promise 实例是这样的:

const promiseInstance = new Promise<void>((resolve,reject)=>{
  // ...
})

现在,我们也能够通过 Promise.withResolvers 来创建实例:

const { promise, resolve, reject } = Promise.withResolvers<void>()

但它并不是为了替代原有创建 Promise 实例的方式而出现,引入它实际上是为了 打破 构造器模式创建 Promise 实例时,resolvereject 方法只能在构造器回调函数中使用的 限制

在它出现之前,我们也可以通过如下代码实现相同的效果:

let resolve, reject
const promise = new Promise((res, rej) => {
  resolve = res
  reject = rej
})

只是 Promise.withResolvers 更简洁,同时标准化了该实现。

适合运用的场景?

Promise.withResolvers 使得 Promise 实例与其 resolvereject 方法处于同一作用域内,也就解放了只能在构造器回调中调用的限制,执行器中的逻辑代码可以被进一步拆分简化,同时减少嵌套层级。

适合运用的场景:在重复的事件中重复使用,特别是在处理流和队列时。

比如如下事件聚合管理器的例子:

// 代码场景来源:https://pawelgrzybek.com/deferred-javascript-promises-using-promise-withresolvers

/**
 * 创建事件聚合管理器
 * @param eventMaxCount 最大事件数,当事件队列数达到该值时,标记为完成聚合并返回事件队列
 */
function createEventsAggregator(eventMaxCount: number) {
  const events: string[] = []
  const { promise, resolve, reject } = Promise.withResolvers()

  return {
    /** 添加推送事件的方法 */
    add: (event: string) => {
      // 如果当前事件队列数小于最大事件数,则添加事件
      if (events.length < eventMaxCount) events.push(event)
      // 如果当前事件队列数达到最大事件数,则标记为完成聚合并返回事件队列
      if (events.length === eventMaxCount) resolve(events)
    },
    /** 取消聚合事件的方法 */
    abort: () => reject("Events aggregation aborted."),
    /** 事件聚合的promise实例 */
    events: promise,
  }
}

const eventsAggregator = createEventsAggregator(3)
eventsAggregator.events
  .then(events => console.log("Resolved:", events))
  .catch(reason => console.error("Rejected:", reason))

eventsAggregator.add("event-one")
eventsAggregator.add("event-two")
eventsAggregator.add("event-three")
// Resolved: [ "event-one", "event-two", "event-three" ]

如何在vite项目中实现兼容?

image.png

从 MDN 上对于该 API 的浏览器兼容性介绍上可以看到,Chrome 浏览器最低支持的版本为 119,对于不支持该特性的浏览器环境,我们需要使用 polyfill 来保证代码的兼容性。

core-js 已经为我们提供了polyfill,我们需要做的是在项目中进行兼容配置,这里以 vite 项目为例。

vite 项目中,常用 @vitejs/plugin-legacy 插件实现项目打包的代码兼容功能。 如果考虑打包性能问题,可以使用 vite-plugin-legacy-swc,其打包耗时相比于 @vitejs/plugin-legacy 有很大的提升,打包后的资源体积也略微少一点。

首先,我们新建一个 vite 项目。

pnpm create vite

删除冗余模板代码,只在项目入口文件处保留如下关于 Promise.withResolvers 的测试代码,用于验证兼容性。

// 项目入口文件 main.ts
const { promise, resolve } = Promise.withResolvers<string>()

setTimeout(() => {
  resolve("done")
}, 500)

promise.then(res => {
  console.log("Promise.withResolvers-resolved:", res)
})

执行 pnpm run build 打包,打包结果如下: image.png

运行预览打包资源 pnpm run preview,打开控制台可以看到: image.png

而运行在 chrome119版本以下的浏览器环境会存在报错,报错原因是 Promise 对象上没有 withResolvers 方法。

下面我们引入 vite-plugin-legacy-swc 插件并在 vite.config.ts 配置文件中使用,

pnpm add vite-plugin-legacy-swc -D

vite.config.ts 文件中配置使用:

import { defineConfig } from "vite"
import legacy from "vite-plugin-legacy-swc"

export default defineConfig({
  base: "./",
  plugins: [
    legacy({
      modernPolyfills: ["esnext.promise.with-resolvers"],
      // 禁用为兼容传统浏览器生成的legacy chunks,仅将polyfill注入modern构建
      renderLegacyChunks: false
    })
  ]
})

这里有必要说明一下,因为 Promise.withResolvers API 在 chrome119 版本才支持,所以这里需要使用 modernPolyfills 配置项为现代版本浏览器生成 polyfills 块。

至于 polyfill 的兼容模块写法规则,比如这里Promise.withResolvers对应模块规则为什么是esnext.promise.with-resolvers,可到 UNPKG - core-js 上对应查找,只有符合规则在项目打包时才能定位到 core-js 中相应的兼容模块。

执行 pnpm run build 打包,打包结果如下,可以看到相比于兼容前多了一个 polyfills 文件:

image.png

这样便可成功兼容支持 ES modules 的现代浏览器了😋。

加料 - 兼容插件打包对比

上面既然提到了 @vitejs/plugin-legacyvite-plugin-legacy-swc 两个 vite 关于兼容的插件,那这里不妨再花一点时间进行总结对比。

关于兼容配置,以上兼容配置写法同样适用于@vitejs/plugin-legacy插件,这里仅需再安装@vitejs/plugin-legacy依赖替换掉vite-plugin-legacy-swc即可。

未兼容@vitejs/plugin-legacy兼容方案vite-plugin-legacy-swc兼容方案
打包耗时88ms504ms288ms
打包资源大小index.html:0.39kB
index.js:0.86kB
index.html:0.48kB
index.js:0.86kB
polyfills.js:9.97kB
index.html:0.48kB
index.js:0.86kB
polyfills.js:9.62kB

可以看到,vite-plugin-legacy-swc插件的打包性能相对更高。

结语

首次发文,感谢你的阅读支持❤️,希望本文能帮助到各位。如有不足或错误之处,欢迎批评指正~