作为 Modern JavaScript 基础设施的一部分,Promises 对前端开发者而言异常重要。它是 async/await 语法的基础,是 JavaScript 中处理异步的标准形式。并且,未来的 Web API,只要是异步的,都会以 Promises 的形式出现。

1、实现 Promises/A+ 规范

1.1、前期工作

An open standard for sound, interoperable JavaScript promises https://promisesaplus.com

通过上面的地址,可以查看规范内容。

通过 npm install promises-aplus-tests ,可以下载测试套件。

通过 npm run test 运行测试套件。

1.2、了解术语

规范的第一部分,描述了几个术语的意思。

promise 是一个包含 then 方法的对象或函数,该方法符合规范指定的行为。

thenable 是一个包含 then 方法和对象或者函数。

value 就是任意合法 JS 值。

exception 就是 throw 语句抛出的值。

reason 是一个指示 promise 为什么被 rejected 的值。

这部分没有需要落实到代码的地方,继续看下去。

1.3 Promise 状态

promise 有 3 个状态,分别是 pending, fulfilled 和 rejected。

在 pending 状态,promise 可以切换到 fulfilled 或 rejected。

在 fulfilled 状态,不能迁移到其它状态,必须有个不可变的 value。

在 rejected 状态,不能迁移到其它状态,必须有个不可变的 reason。

落实到代码上,大概像上面那样:

有 3 个常量 pending, fulfilled, rejected,

一个 Promise 构造函数,有 state 和 result 两个属性。

当 state 为 fulfilled 时,result 作为 value 看待。

当 state 为 rejected 时,result 作为 reason 看待。

一个 transition 状态迁移函数,它只会在 state 为 pending 时,进行状态迁移。

如上,其实并没有多少自由发挥的空间。不管由谁来编写,仅仅是变量名,代码行数上的微小差异。

1.4、Then 方法

promise 必须有 then 方法,接受 onFulfilled 和 onRejected 参数。

那就像下面那样,新增一个 then 的原型方法。

onFulfilled 和 onRejected 如果是函数,必须最多执行一次。

onFulfilled 的参数是 value,onRejected 函数的参数是 reason。

then 方法可以被调用很多次,每次注册一组 onFulfilled 和 onRejected 的 callback。它们如果被调用,必须按照注册顺序调用。

那就像上面那样,为 Promise 新增一个 callbacks 数组记录。

then 方法必须返回 promise。

那 then 实现丰富化成下面这样:

在 then 方法里,return new Promise(f),满足 then 必须 return promise 的要求。

当 state 处于 pending 状态,就储存进 callbacks 列表里。

当 state 不是 pending 状态,就扔给 handleCallback 去处理。

至于 handleCallback 是什么。其实不重要,我们只需要知道,它一定存在。我们总得做一些处理,不是写死在 then 函数里,就是在外部的辅助函数里。

至于为啥要套个 setTimeout 呢?

因为 then 方法里,还有一个重要约束是:

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

我们不是在 JS 引擎层面实现 Promises,而是使用 JS 去实现 JS Promises。在JS里无法主动控制自身 execution context stack。可以通过 setTimeout/nextTick 等 API 间接实现,此处选用了 setTimeout。

then 方法返回的 promise,也有自己的 state 和 result。它们将由 onFulfilled 和 onRejected 的行为指定。

这正是 handleCallback 要做的部分。

handleCallback 函数,根据 state 状态,判断是走 fulfilled 路径,还是 rejected 路径。

先判断 onFulfilled/onRejected 是否是函数,如果是,以它们的返回值,作为下一个 promise 的 result。

如果不是,直接以当前 promise 的 result 作为下一个 promise 的 result。

如果 onFulfilled/onRejected 执行过程中抛错,那这个错误,作为下一个 promise 的 rejected reason 来用。

then 方法核心用途是,构造下一个 promise 的 result。

我们的代码,几乎没有多余的处理逻辑,忠实的完成规范指定的行为。

1.4、The Promise Resolution Procedure

从上面的截图里,我们还看到了 The Promise Resolution Procedure 的说法。

它描述的是,一些特殊的 value 被 resolve 时,要做特殊处理。这个特殊处理,规范也明确描述了。

第一步,如果 result 是当前 promise 本身,就抛出 TypeError 错误。

第二步,如果 result 是另一个 promise,那么沿用它的 state 和 result 状态。

第三步,如果 result 是一个 thenable 对象。先取 then 函数,再 call then 函数,重新进入 The Promise Resolution Procedure 过程。

最后,如果不是上述情况,这个 result 成为当前 promise 的 result。

用代码描绘起来,如上所示,按照规范描述的顺序,编写 3 个 if。

第一个判断 result 是不是 promise 本身,是就抛 TypeError 错误。

第二个判断 result 是不是 promise 类型,是就调用 then(resolve, reject) 取它的 value 或 reason。

第三个判断 result 是不是 thenable 对象,是就先取出 then,再用 new Promise 去进入 The Promise Resolution Procedure 过程。

若都不是,则直接 resolve result。

1.5、整合剩余部分

至此,所有重要部分,都已经被处理。

1)我们有了 transition 对单个 promise 进行状态迁移。

2)我们有了 handleCallback ,在当前 promise 和下一个 promise 之间进行状态传递。

3)我们有了 resolvePromise,对特殊的 result 进行特殊处理。

接下来,我们只需要整合一下,把各部分衔接起来即可。

其中,Promise 构造函数,扩充如下:

构造 onFulfilled 去切换到 fulfilled 状态,构造 onRejected 去切换到 rejected 状态。

构造 resolve 和 reject 函数,在 resolve 函数里,通过 resolvePromise 对 value 进行验证。

配合 ignore 这个 flag,保证 resolve/reject 只有一次调用作用。

最后将 resolve/reject 作为参数,传入 f 函数。

若 f 函数执行报错,该错误就作为 reject 的 reason 来用。

transition 函数扩充如上,当状态变更时,异步清空所有 callbacks。

之前我们已经实现了 handleCallback,实现 handleCallbacks 只需要一个循环。

运行测试套件后,全部 passing。

1.6、ES2015 Promises

Promises/A+ 规范跟 ES2015 Promises 不完全等价。在 A+ 规范里,并没有描述 catch 方法,以及 Promsie.resolve, Promise.reject, Promise.all, Promise.race 等静态方法。

甚至,new Promise 这种用法都不是 A+ 规范的内容,只是恰好我们现在用 ES2015 Promises 风格去实现。

ES2015 Promises 兼容 Promises/A+ 规范,并做了自己的扩充。

有了 then 方法,我们可以很容易实现 ES2015 Promises 的几个扩充方法。

catch 方法和 resolve/reject 静态方法的实现如上所示。Promise.all 和 Promise.race 的实现

2.2、promises 是比 callback 更先进的异步方案?

callback -> promise -> generator -> async/await

2.2.1、promises 也属于 callback style 的一种

在 Promises/A+ 规范的第一段,我们能看到一个明确的表述:

promise 是通过 then 方法去注册 callbacks,其中 onFulfilled callback 处理 value,而 onRejected callback 处理 reason。

callback style,通常是指 nodejs 那种 Error-First Callbacks,或者其它 raw callback。

2.2.2、generator function 也是一种 callback style

基于 generator + promise 的异步解决方案,可以实现用同步的写法,编写异步代码的效果。比如用 tj 的 co 库:

  1. co(function* () {

  2. var result = yield Promise.resolve(true);

  3. return result;

  4. }).then(function (value) {

  5. console.log(value);

  6. }, function (err) {

  7. console.error(err.stack);

  8. })

2.3、async/await 是异步终极解决方案?

我不太确定当人们说 async/await 是异步终极解决方案时,所描述的终极在什么维度上衡量。

2.3.1 generator function 比 async function 更普适

语义化和标准化,不意味着能力的增强,它也有可能导致能力的减弱。

async/await 是能力减弱的案例。

generator function 即能支持同步行为,也能支持异步行为。

async function 只支持异步行为。

对于 tj 的 co 库来说,promise 只是它最主要的异步数据源,co 还能从其它异步数据源中获取结果。比如 thunk 函数。

如上所示,当 yield 一个 thunk 函数时,co 会传递 done 这个 nodejs 风格的 callback 函数。

如果愿意,我们还可以支持 yield rxjs 的 observable 等对象。

这是因为 co 是一个 library 里,它可以尽可能利用 generator function 的一切特性,实现想要的拓展功能。

而 async/await 是一个新的语法,它必须建立在标准化的基础上,它必须拥有一致的语义。它需要做很多取舍,通过放弃对非标准化对象的支持,换取清晰的语义。

因此,async/await 只能从 promise 对象中获取异步数据结果,相比 co 是一种能力上的降低。

2.3.2 裸写 promise 比 async/await 更灵活

尽管 90% 以上的异步场景下,async/await 都能胜任;然而,还是有一些场景,裸写 promise 更加灵活。

最典型的案例就是并行的 promise 处理。

如上,await 关键字总是串行,当我们想要依次获取 a, b 时,写起来是很简单。如果我们想同时获取 a 和 b 并等待其结果,await 关键字却难以处理。

我们得自行通过 Promise.all 将多个 promise 包装成单个。

曾经有个 await* 的提案,作为 Promise.all 的语法糖。不过,并没有得到落地

仔细一想,很容易理解为什么上图的做法,难以落地。Promise.all 只是 promise 的其中一种组合方式,还有 Promise.race,Promise.allSettled 等组合。

如果为每个组合方式都分配一个符号作为语法糖,代码将更难以阅读。

因此,当需要组合多个 promise 时,裸写 promise 是一个必要措施。

此外,async/await 是语法,不是值,因此它不能被存储和传递。而 promise 对象,可以存储在内存里,可以作为参数在函数中传递。

这种灵活性,在一些特殊场景下,可以带来便利。比如,我们可以通过缓存 promise 来缓存异步结果。

如上所示,我们建立了一个 map,存储 url -> promise 的映射。每次 get url 时,都查一下缓存。

通过 async/await 语法的话,promise 对象被隐藏起来了。我们无法获取。最多等结果返回后,缓存 url -> result 的映射。

然而,这种做法的缓存覆盖面有空隙。当 get 请求触发,但结果还没抵达的过程中,又触发了多个相同的请求,这些请求无法命中缓存。

如果我们缓存的是 promise 对象,那么利用 promise 对象可以多次调用 then 方法的特性,我们能做到让所有 get url 获取到同一份异步请求结果。

2.3.3 裸写 callback 比 promise 更灵活

promise 的 then 只支持 onFulfilled 和 onRejected 两种 callback 路径,属于对所有可能的 callback 路径的简化。

比如前面提到的 rxjs,observer 和 subscriber 有 { next, error, complete } 三个 callback 路径,能比 promise 处理更多 cases。

如果愿意手动管理 callback,在理论上我们能做到比 promise 更强大和灵活。

比如 cyclejs 作者提出的 callbag 模式,仅用多个 callback 函数的组合,就实现了 rxjs-like 的 observables and iterables。

感兴趣的同学,可以访问上面的链接,了解 callbag。

受到 callbag 的启发,rxjs v7.0 版本正在用相似的模式,进行内部重构。他们称这个模式为 Functional Observables。

我们可以看到,async/await 反而是表达能力最弱的一个,callback 则是最强的一个。

JavaScript 的异步方案演进史:

Raw Callback Style -> Promise Callback Style -> Generator Callback Style -> Async/Await Callback

并非表达能力不断增强的过程,而是对开发者的友好程度不断增加的过程。

Rxjs Callback Style 因为没有语法糖的支撑,且 operators 极多,出名的对新手不友好,因此一直难以成为主流方案。

而 Callbag Style 更为反人类,连源代码都不利于阅读。只适合由资深的开发者编写,隐藏在 library/framework 内部。

2.3.4 语法糖是有代价的

到目前为止,我们知道 async/await 语法可以视为多个 callback 函数组合的语法糖,可以简化我们编写的异步代码的复杂性。

不过,这不是没有代价的。

JS 的编译器需要处理大量的场景,要识别关键字,要准确的处理异步的 throw error 和同步的 throw error 的差异。要让 async/await 能跟普通函数协调的工作,能跟 generator function 协调的工作。

async/await + generator function 又将组合出一个新的 async generator function,异步生成器。

如上图所示,通过 async generators + for-await 语法,我们可以同时获得async/await 的异步处理能力,和 generator 输出多个值的能力。

在 JavaScript 里,有多种函数类型:

1) plain function 普通函数 2) arrow function 箭头函数 3) generator function 生成器函数 4) async function 异步函数 5) async arrow function 异步箭头函数 6) async generator function 异步生成器函数

不断新增的函数类型和语法,对编译器的迭代和优化提出了巨大挑战,也对 ECMAScript 语言新增特性带来了问题。所以我们能看到 async arrow function,却没有看到 generator arrow function,以及 async generator arrow function。

将来增加更多函数类型,跟之前的函数类型进行排列组合,数量将会越来越多,协调多种函数类型将变得越来越难。

上述 Paper 描述了具备 Algebraic Effects 特性的 Koka 语言,可以通过 algebraic effect handlers 模拟包括 async/await 在内的诸多特性,以 library 而非 language syntax 的形式提供。不会显著增加编译器的负担。

React 即将发布的 Suspense 特性,即采用了上述能力,它内部通过 JS 里一些比较 hack 的方式去模拟 Algebraic Effects。可以实现在 render 函数里不需要 await 一个异步操作,也能通过某种途径,获取到异步结果,同时又不会影响 render 函数的幂等要求。

对 Promises/A+ 规范的研究 ------引用的更多相关文章

  1. 一起学习造轮子(一):从零开始写一个符合Promises/A+规范的promise

    本文是一起学习造轮子系列的第一篇,本篇我们将从零开始写一个符合Promises/A+规范的promise,本系列文章将会选取一些前端比较经典的轮子进行源码分析,并且从零开始逐步实现,本系列将会学习Pr ...

  2. 【翻译】Promises/A+规范

    目录 介绍 译文 1. 术语(Terminology) 2. 要求(Requirements) 2.1 Promise状态 2.2 then方法 2.3 Promise解析程序 3. 注释 3.1 p ...

  3. Promises/A+规范

    为什么需要异步编程方式 一个函数执行之后,在它后面顺序编写的代码中,如果能够直接使用它的返回结果或者它修改之后的引用参数,那么我们通常认为该函数是同步的. 如果一个函数的执行结果或者其修改的引用参数, ...

  4. JavaScript中Promises/A+规范的实现

    Promises是一种异步编程模型,通过一组API来规范化异步操作,这样也能够让异步操作的流程控制更加容易. 下面的代码是假设执行一个异步队列,每一项都会使用上一项返回的数据: function ne ...

  5. CommonJS Promises/A规范

    本文来自四火哥的翻译 CommonJS是一组javascript编程规范,而promise是其中之一. 简而言之,promises是一种令代码的异步行为变得更加优雅的软件抽象.在基本的定义中,代码可能 ...

  6. JS魔法堂:剖析源码理解Promises/A规范

    一.前言 Promises/A是由CommonJS组织制定的异步模式编程规范,有不少库已根据该规范及后来经改进的Promises/A+规范提供了实现 如Q, Bluebird, when, rsvp. ...

  7. 前端翻译:Promises/A+规范

    原文地址:https://promisesaplus.com/ 本篇为原文翻译+个人理解,若有谬误请各位指正,谢谢. 尊重原创,转载请注明来自:http://www.cnblogs.com/fsjoh ...

  8. 对GraphQL-BFF:微服务背景下的前后端数据交互方案的研究-------引用

    随着多终端.多平台.多业务形态.多技术选型等各方面的发展,前后端的数据交互,日益复杂. 同一份数据,可能以多种不同的形态和结构,在多种场景下被消费. 在理想情况下,这些复杂性可以全部由后端承担.前端只 ...

  9. 对Webpack 应用的研究-----------------引用

    对大多数 Web 应用来说,页面性能直接影响着流量.这是一个经常为我们所忽视的事实.用户长时间的等待流失的不仅仅是跳出率.转化率,还有对产品的耐心和信赖.很多时候我们没有意识到性能问题,那是因为平常开 ...

随机推荐

  1. Linux进程:管理和调度

    一:进程管理 进程.轻量级进程和线程 通常定义:进程是程序执行时的一个实例. 这个很像类和实例对象的关系.从内核来看:进程的目的就是担当分配系统资源(CPU,内存等)的实体. 当进程创建时,它几乎和父 ...

  2. 在Linux命令行模式安装VMware Tools

    在Linux命令行模式安装VMware Tools 方法/步骤1: 首先启动CentOS 7,在VMware中点击上方“VM”,点击“Install VMware Tools...”(如已安装则显示“ ...

  3. ubuntu16.04离线安装nvidia-docker2

    目前需要离线对ubuntu 进行封装docker环境 在熟悉docker环境过程中,有网络条件下,还处于懵逼状态 离线安装…… 大佬救救我 来了: 首先下载安装docker最新版 我的OS是Ubunt ...

  4. 【神经网络与深度学习】Caffe训练执行时爆出的Check failed: registry.count(t ype) == 1 (0 vs. 1) Unknown layer type

    自己建立一个工程,希望调用libcaffe.lib ,各种配置好,也能成功编译,但是运行就会遇到报错 F0519 14:54:12.494139 14504 layer_factory.hpp:77] ...

  5. 【Qt开发】Qt在QLabel(QWidget)鼠标绘制直线和矩形框

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://devbean.blog.51cto.com/448512/243546 说实话, ...

  6. 文件的上传(1)(表单上传和ajax文件异步上传)

    文件的上传(表单上传和ajax文件异步上传) 项目中用户上传总是少不了的,下面就主要的列举一下表单上传和ajax上传!注意: context.Request.Files不适合对大文件进行操作,下面列举 ...

  7. Linux ulimit 命令 限制系统用户对 shell 资源的访问

    ulimit命令用来限制系统用户对 shell 资源的访问,常见用法如下: [root@MongoDB ~]# ulimit -a // 查看当前所有的资源限制 [root@MongoDB ~]# u ...

  8. C语言博课作业11

    一.本周作业头 这个作业属与那个课程 C语言程序设计I 这个作业要求在哪里 https://edu.cnblogs.com/campus/zswxy/CST2019-3/homework/10130 ...

  9. Nginx_Ubuntu

    一. 基本步骤 1.1 环境准备 开始前,请确认gcc g++开发类库是否装好,默认已经安装. 注: 等待linux下载更新功能准备好了 重启系统 在执行下载安装命令,如执行命令没有问题可以继续往下走 ...

  10. 好问题:count(1)、count(*)、count(列)有什么区别?

    执行效果: 1.  count(1) and count(*) 当表的数据量大些时,对表作分析之后,使用count(1)还要比使用count(*)用时多了! 从执行计划来看,count(1)和coun ...