捋捋Promise

420 阅读8分钟

本文原创:duxiaoxue

一些引用

一旦一个 Promise 决议,不论是现在还是将来,下一个步骤总是相同的。

既然 Promise 是通过 new Promise(..) 语法创建的,那你可能就认为可以通过 p instanceof Promise 来检查。但遗憾的是,这并不足以作为检查方法。识别 Promise(或者行为类似于 Promise 的东西)就是定义某种称为 thenable 的东西,将其定义为任何具有 then(..) 方法的对象和函数。我们认为,任何这样的值就是 Promise 一致的 thenable。

Promise 模式构建的可能最重要的特性:信任。

即使是立即完成的 Promise(类似于 new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。也就是说,对一个 Promise 调用 then(..) 的时候,即使这个 Promise 已经决议,提供给 then(..) 的回调也总会被异步调用。

一个 Promise 决议后,这个 Promise 上所有的通过 then(..) 注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。

-- YDKJS

先说异步

所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。 ... 相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

--阮一峰《ES6入门》

在 Promise 之前:回调函数

setTimeout(() => {
    // statements
}, 1000)
var xhr = new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";

xhr.open(method, url, true);
xhr.onreadystatechange = function () {
    if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send();

缺点:

  • 大量的嵌套导致的回调地狱让人难以理解代码的实际发生顺序
  • 控制反转产生信任问题,回调执行情况无法保证

大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。 回调函数的调用控制交与第三方函数内部,无法保证回调函数一定会被正确调用。可能出现很多异常情况:所需参数传递错误、调用回调过早或过晚、调用回调次数太多或太少、吞掉可能出现的错误与异常等等。 回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。 ... 我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。 也需要一个通用方案来解决信任问题。

-- YDKJS

Promise登场

promise的思想

如果我们不把自己程序的 continuation 传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么。这种范式就称为Promise

Promise 决议后就是外部不可变的值,我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。特别是对于多方查看同一个 Promise 决议的情况,尤其如此。一方不可能影响另一方对 Promise 决议的观察结果。 不可变性听起来似乎一个学术话题,但实际上这是 Promise 设计中最基础和最重要的因素。

Promise 是一种封装和组合未来值的易于复用的机制。

-- YKDJS

社区推动

最早的Promise是由社区首先提出和实现的,早期比较有名的有jQuery的Deferred对象、bluebird、Q等等。ES6也将Promise纳入语言标准,提供了原生的Promise对象。

最新的规范是在2014年发布的promise/A+

Promise/A+规范

一个promise指示一个异步操作的最终结果。与promise交互的主要方法是通过它的then方法,在then方法中注册了接收promise的最终值或是无法被完成原因的回调。

Promise/A+只专注于提供可操作的then方法的规范。

规范中指出,ECMAScript语言规范中的Promise对象基于本规范还实现了许多额外的要求,也就是说我们可以自行实现一个完全遵守promise/A+规范但不必完全遵守ECMAScript语言规范的Promise,某种程度上说ES6里面的Promise也只是许多promise/A+规范实现的一种。

术语

  • promise: 带有按规范实现的then方法的对象或函数
  • thenable: 定义了then方法的对象或函数
  • value: 任何合法的JavaScript值(包括undefinedthenable或是promise),终值
  • exception: 使用throw抛出的值
  • reason: 指示promise被拒原因的值,拒因

规范要求

(一). Promise的状态

一个`promise`必须是这三种状态之一:

- `pending`(进行中、等待中)
- `fulfilled`(被完成、被执行)
- `rejected`(被拒绝)

状态的迁移:

1. 当`pending`时,`promise`的状态可以变到`fulfilled`或是`rejected`
2. 当`fulfilled`时,`promise`的状态不可再变,同时必须持有一个**不可变**的`value`(终值)
3. 当`rejected`时,`promise`的状态不可再变,同时必须持有一个**不可变**的`reason`(拒因)

这里的**不可变**指的是恒等(即可用 `===` 判断相等),但不意味着更深层次的不可变(如非基本类型值时,只要求引用地址相等)。

(二). then方法

一个`promise`必须提供一个`then`方法以访问其当前值、终值和据因。方法接受两个参数:
```js
promise.then(onFulfilled, onRejected)
```
1. `onFulfilled`、`onRejected`均为可选参数,如果不是函数类型,则必须被忽略
2. 如果`onFulfilled`是个函数:在promise被完成前不可被调用;在promise被完成后必须被调用,以promise的终值作为第一个参数;不能被多次调用。
3. 如果`onRejected`是个函数:在promise被拒绝前不可被调用;在promise被拒绝后必须被调用,以promise的据因作为第一个参数;不能被多次调用。
4. 调用时机

    保证`onFulfilled`、`onRejected`在`then`被调用的那轮事件循环之后的新执行栈中异步执行。

    这一点可以使用宏任务(macro-task)机制如`setTimeout`、`setImmediate`,或微任务(micro-task)机制如`MutationObserver`、`process.nextTick`来实现。
5. 调用要求

    `onFulfilled`和`onRejected`必须被作为函数调用(即没有 this 值)(在严格模式中,函数`this`的值为`undefined`;在非严格模式中其为全局对象)
6. `then`方法可以被同一个promise调用多次

    - 当promise被完成时,所有`onFulfilled`按注册顺序依次回调
    - 当promise被拒绝时,所有`onRejected`按注册顺序依次回调

7. `then`方法必须返回一个promise对象

    ```js
    promise2 = promise1.then(onFulfilled, onRejected);
    ```
    - 如果`onFulfilled`、`onRejected`返回一个值x,则运行下面决议`promise`的过程:`[[Resolve]](promise2, x)`
    - 如果`onFulfilled`、`onRejected`抛出一个异常e,则`promise2`必须拒绝执行,并返回拒因e
    - 如果`onFulfilled`不是函数且`promise1`成功执行, `promise2`必须成功执行并返回相同的值
    - 如果`onRejected`不是函数且`promise1`拒绝执行,`promise2`必须拒绝执行并返回相同的拒因

(三). 决议promise的过程(即[[Resolve]](promise, x)的具体实现)

1. 如果`x`与`promise`相等,以`TypeError`为拒因拒绝执行promise
2. 如果`x`为`Promise`的实例,则使`promise`接受`x`的状态
    - 如果`x`进行中,`promise`也需保持进行中的状态直至`x`被完成或拒绝
    - 如果`x`被完成,用相同的值执行`promise`
    - 如果`x`被拒绝,用相同的据因拒绝`promise`
3. 如果`x`为函数或者对象
    1. 把`x.then`赋值给`then`
    2. 如果取`x.then`的值时抛出错误`e` ,则以`e`为据因拒绝`promise`
        1. 如果`then`是函数,将`x`作为函数的作用域`this`调用之。传递两个回调函数作为参数,第一个参数叫做`resolvePromise`,第二个参数叫做`rejectPromise`:

            - 如果`resolvePromise`以值`y`为参数被调用,则运行`[[Resolve]](promise, y)`
            - 如果`rejectPromise`以据因`r`为参数被调用,则以据因`r`拒绝`promise`
            - 如果`resolvePromise`和`rejectPromise`均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用
            - 如果调用`then`方法抛出了异常`e`:
                - 如果`resolvePromise`或`rejectPromise`已经被调用,则忽略之
                - 否则以`e`为据因拒绝`promise`
        2. 如果`then`不是函数,以`x`为参数执行`promise`
    3. 如果`x`不为对象或者函数,以`x`为参数执行`promise`

测试

如果按照规范自行实现Promise,可以使用下面官方提供的工具监测是否符合规范。

练习题

掘金上可以找到一些不错的练习题,用来巩固知识再好不过了。

参考