文艺复兴:2022 年的 Promise 库 NativeBird

Promise/A+ 规范迄今已有十年历史,社区也有 Bluebird 这样 2w+ star 的权威第三方实现。但 Bluebird 虽然提供了许多便捷的异步方法,其 minifiy 后体积仍有 80KB 以上。在原生 Promise 早已普及的今天,是否有可能直接基于原生 Promise 实现异步库,从而兼顾便捷的异步流程控制与极致的轻量级呢?NativeBird 就做到了这一点。

NativeBird 直接继承了原生 Promise,通过标准的 Promise API 实现了各种 Bluebird API。这些 NativeBird 所扩展的 API 均完整通过了 Bluebird 中相应的测试用例,这使得它很容易直接无缝替代 Bluebird 而无需业务改造。作为试验,个人已经基于 NativeBird 在公司编辑器项目中 drop-in 地实现了去 Bluebird 化的迁移。目前项目在测试环境中生命体征稳定,其中一些基于 Bluebird 自定义的二次封装(如支持去重的异步队列)也能正常工作。

作为供内部项目迁移而实现的最小版本,当前的 NativeBird API 包括如下:

当然,目前 Bluebird 中还有许多如 Promise.reducePromise.some 等其他 utils API 是 NativeBird 尚未实现的。但从原理上说,除了 Promise.cancel 这样有违原生 Promise 设计的特性,对于大多数剩余的 Bluebird API,NativeBird 要实现它们都并不是问题。重点在于在 NativeBird 这个新项目里,可以去粗取精地重新审视 Bluebird 中到底有哪些 API 是真正实用而值得继续保留的。例如像 Promise.promisifyAll 这样常用于给经典 Node 回调风格 API 填坑的方法,就完全可以因为新版 Node 中 fs.promise 的存在而废弃。

一个能体现 Bluebird 实用之处的例子,就是 Promise.map 了。它可以通过 concurrency 配置,在并发量为 M 的限制下请求 N 个任务。这个 API 在 NativeBird 中的实现方式大致如下:

// 参数格式有点改动,但这不重要
NPromise.map = (iterable, fn, concurrency) => {
  let index = 0;
  const iterator = iterable[Symbol.iterator]();
  // 这里最多只会有 M 个 promise
  const promises = [];
  // 这里最终会填满 N 个任务的结果
  const results = [];

  // 启动前 M 个异步任务
  while (concurrency-- > 0) {
    const promise = wrappedMapper();
    if (promise) promises.push(promise);
    else break;
  }

  // 这里虽然只有 M 个 promise
  // 但它们 resolve 后会继续从迭代器中取值,共取 N 个
  return NPromise.all(promises).then(() => results);

  // 把迭代器里的值 unwrap 出来然后 map,意会一下
  function wrappedMapper() {
    const next = iterator.next();
    if (next.done) return null;
    const i = index++;
    // 注意 next.value 既可能是值也可能是 promise
    return Promise.resolve(next.value).then(async (unwrapped) => {
      const mapped = await fn(unwrapped, i);
      results[i] = mapped;
      return wrappedMapper(mapped);
    });
  }
}

业务中依赖 Promise.map 的逻辑实际上广泛存在,说明 Bluebird 的扩展确实在实际项目中有实用价值。但至于其他的 API 这里就不再展开了,毕竟国内前端社区每隔几天就能涌现出一篇全新的手写 Promise 文章。

相比于 NativeBird 目前仅有 100 多行的代码实现,个人更推荐大家去阅读它的测试用例。这些直接继承于 Bluebird 的用例已经由社区维护多年,其中相当好地展示了:

  • 应该怎么使用某个 API
  • 应该怎么覆盖函数实现的某一部分
  • 应该怎么设计边界与异常情况
  • 应该怎么给用例命名

这些用例的行数是目前 NativeBird 代码量的 5 倍以上。没有它们的支持,是不可能做到一天实现兼容原型,一天完成业务迁移的。另外,NativeBird 的 dts 类型定义也直接从 @types/bluebird 裁剪而来,其中也有不少体操技巧值得学习。

NativeBird 其实只是一个为解决公司技术债而简单实现的小作品。在当前满足公司内部项目的迁移需求后,个人没有太多精力继续移植剩余的 Bluebird API。希望有兴趣的社区同学能参与贡献,Make Promise Great Again!

Star NativeBird


历史彩蛋:Promise A+ 规范 1.0 发布于 2012 年 12 月 6 日第二天北原春希被分配了去欧洲出差的任务,当晚雪菜提出想去斯特拉斯堡……然后就是一段 Promise.race([冬马, 雪菜]) 的故事了。

屏幕截图 2022-05-27 053941.jpg 春哥告诉你写代码别乱加延时,传达不到的 Promise 请务必 reject

猜你喜欢

转载自juejin.im/post/7115207232798752782