Promise

744 阅读8分钟

Promise 在面试中经常会被问到,所以我就深入研究了一下,写下这篇文章。

Promise 是 ES6 新增的对象,是异步编程的一种解决方案。

Promise 发明的理由

在 Promise 之前,JavaScript 中常见的处理异步的方式是传入 回调函数。原生的 setTimout(cb, delay) 定时器函数就是通过回调函数实现异步编程。

假设 fnX 指的是异步函数(X表示),能接受函数参数,在一段时间后会执行传入的这个函数。当多个异步是有依赖关系,需要 依次顺序执行的时候,函数的调用就会变成类似下面的这个样子:

fn1(() => {
    fn2(() => {
        fn3(() => {
            fn4(() => {
                //...
            })
        })
    })
})

这就是所谓的 回调地狱。在回调很多的情况下,代码可读性低,也难以维护,非常容易出现 bug。

于是,ES6 中添加了 Promise ,从底层上来解决这个问题。

Promise/A+

我们偶尔会看到 Promises/A+ 这个术语。其实它是一个规范,叙述了 Promise 的标准,供参考者参考。在 Promises/A+ 之前,其实还有 Promises/A 标准,Promises/A+ 规范是对先前 Promises/A 规范条款的澄清明确,一方面对其扩展进而覆盖事实行为;另一方面删除了其中没有规范化或者存在问题的规范条款。

Promise 的状态

Promise 是有状态的,当它被创建时,状态为 pending。在之后状态会变为 fulfilledrejected,表示异步操作最终成功还是失败。

此外,Promise 无法从 fulfilled 状态变成 rejected;反之也不可以。也就是说, Promise 的状态变化只有两种情况:

pending -> fulfilled
pending -> rejected

术语:fulfilled 和 rejected 状态可以统称为 settled 状态或 resolved 状态。(因为叙述的时候如果老是说 "变成 fullfilled 或 rejected 状态",就会显得又长又累赘。)

Promise 对象的方法

Promise 构造函数

首先我们了解一下 Promise 的构造函数,它的作用是包装还未支持 Promises 的函数。

new Promise( function(resolve, reject) {...} /* executor */  );

我们需要通过 new 关键字来创建一个 Promise 对象,且必须传入一个函数 executor 作为参数。executor 函数有两个参数:resolvereject。它们是两个函数,被执行时,分别会将 Promise 的状态转为 fulfilledrejected

这里有几点需要注意:

  • 一旦 Promise 状态变成 fulfilledrejected,后续执行 resolve 或 reject 没有任何效果。
  • 如果同时执行了 resolve 和 reject 函数,Promise 的状态会变为先执行的函数对应的状态,因为 fulfilledrejected 之间无法进行状态转化。
  • reject 函数被执行时,会抛出错误。
  • 如果 executor 在 resolve 函数执行前抛出了错误,Promise 的状态会变为 rejected。
  • 抛出的错误可以用返回的 Promise 对象调用 then 和 catch 等方法来捕获(本文后面会讲)。

then

Promise.prototype.then(onfulfilled, onrejected)

当一个 Promise 对象的状态变成 fulfilledrejected 时,该对象的 then 方法绑定的两个函数 onfulfilledonrejected 就会被执行。

let m = new Promise((resolve, reject) => {
    resolve('success');
    // reject('fail');
})
.then(
    /* onfulfilled */
    v => {
        console.log(v)
    }, 
    /* onrejected */
    v => {
        console.log(v)
    }
)

then 方法会返回一个新的 Promise,且它的状态会被设置为前一个 Promise 的状态。所以,我们就可以使用使用 then 方法,写出 链式 风格的代码,摆脱了之前的回调地狱。

then 方法里面添加的函数其实就对应了前面提到的 回调函数 写法里的回调函数。如果 then 里的回调函数中返回的是 Promise,那 then 方法返回的就是该 Promise。如果回调函数的返回值不为 Promise,则返回一个新的 Promise 对象,并将返回值作为下一个 then 方法的回调方法的参数。

onreject 回调函数会捕获 Promise 变成 rejected 状态时,传递的值。如果是 throw new Error(),就会得到一个 error 对象,如果是 rejected(val),就会得到 val 的值。

catch

catch 等价于只有 onRejected 回调函数的 then 方法。

Promise.prototype.catch(onRejected)

等价于:

Promise.prototype.then(undefind, onRejected)

finally

Promise.prototype.finally(onFinally)

需要注意的是 nodejs 在 10.3.0 版本左右才实现了 finally 方法

当 Promise 状态变成 fulfilled 或 rejected 时,执行 finally 方法注册的回调函数。

这里要说明的是,这个 finally 指的是状态变成最终状态,而不是最后触发的意思(和 switch 的 default 不同)。Promise 对象的 then、catch、finally 方法绑定的回调方法的执行顺序,由它们被绑定时的顺序决定。(就像 addEventListener 一样)。

错误传递

const p = new Promise((resolve, reject) => {
    reject();
})
.then(() => {

})
.then(() => {
    rej();
})
.catch(() => {
    console.log('捕获错误')
})

当 Promise 的状态变为 rejected 时,如果不对错误进行捕获,就会抛出错误终止程序运行(当然如果是事件响应函数里的报错,倒是不会终止主要程序),所以我们必须对 Promise 的错误进行捕获。

当一个 Promise 的状态变为 rejected 时,通过 then 方法的 onrejected 方法(等同于 catch 方法),可以将对应的错误捕获。但如果 then 方法中没有提供第二个类型为函数的参数,错误就会通过 then 方法传递给下一个 Promise 对象,直到其中一个对象用 onRejected 方法捕获了该错误。

Promise 的类方法

Promise.resolved(value)

  1. 如果 value 是一个有 then 方法的对象(比如一个 Promise 对象),返回的 Promise 的状态由这个对象来决定。(注意,一个对象有 then 方法,并不能代表它就是 Promise 对象)
  2. 如果 value 是一个 Promise 对象,该方法返回的就是这个 Promise 对象。
  3. 如果 value 不是一个 Promise 对象,则会返回一个状态为 resolved 的 Promise 对象,并提供 value 给返回的 Promise 对象的 then 方法使用。
const m = Promise.resolve(new Promise((resolve) => {
    resolve(1);
}))

m.then(
    v => {
        console.log('success', v);
    }, 
    v => {
        console.log('fail', v);
    }
)

// 输出 success 1

Promise.reject(value)

返回一个状态为 resolved 的 Promise 对象,并提供 value 的值。效果类似 Promise.resolve(value),只是状态不同。

Promise.all(iterable)

all 方法接收一个 可迭代 的对象,如 Array 和 String,然后返回一个新的 Promise 对象。

当 iterable 参数内的所有 Promise 对象都变成 resolved 状态时(或者可迭代对象里一个 Promise 对象都没有),会返回一个新的 Promise 对象,并提供一个和迭代顺序相同的 数组 作为回调函数的传入参数。 如果其中一个 Promise 的状态变为 rejected,就会返回的状态为 rejected 的新的 Promise,且提供的参数不再是数组,而是 reject 对应的返回值。

Promise.race(iterable)

该方法和 all 类似,也需要传入可迭代的对象。

race 的意思是看看谁快(赛跑)。当其中一个 Promise 的状态变为 fulfilled 时,该方法返回的 Promise 对象的状态也变成为 fulfilled,并能得到前者的返回值作为回调函数的参数。

同样,如果其中一个 Promise 变成了 reject 状态,all 返回的 Promise 也会变成 reject 状态。

简单的 Promise 使用

1. 包装 setTimeout 方法。

const mySetTimeout = (delay) => {
    return new Promise((resolve) => {
        setTimeout(resolve, delay);
    })
}

mySetTimeout(1000).then(() => {
    console.log('1000 years later~')
})

2. XHR 请求

假设有一个 xhr 函数,通过回调函数 success 和 fail 来处理请求回来的数据。。

function xhr(data, success, fail) { /* */};

那我们可以这样包装它:

const myXhr = (data) => {
    return new Promise((resolve, reject) => {
        xhr(data, resolve, reject)
    })
}

myXhr(data).then(result => {
    // 成功获得数据,做进一步处理。
}).catch(e => {
    // 失败
})

其他异步方案

当然 JavaScript 的异步编程除了回调函数和 Promise,这里简单讲解一下这些方案:

  • 事件监听:写法类似 f1.on('done', f2)。指一个事件函数执行后,在某个事件触发 done 事件,从而执行绑定的响应事件。前端的各种事件(如click)就是用这种方式。缺点是程序会变成 事件驱动型,运行流程变得不清晰,也就是
  • 发布订阅模式:这个是经典的设计模式中的一种。该模式下,有一个消息中心记录谁进行了任务的订阅和发布,当消息发布时,通知它的订阅者。其实这种方式有点类似前面的事件监听,但它多了一个调度中心,我们可以通过监控它知道程序共有多少个消息,和它们有多少订阅者。
  • async/await:async 函数是 ES8 引入的的,其实它是一种配合 Promise 将代码写出同步代码的样子的方案。某种意义上还是 Promise,只是写法发生了变化。在该方案被提出前,曾经有过一个 Generators/ yield 方案能实现 async/await 的写法,需要用到 co 库。
  • 一些将回调嵌套变成数组等顺序调用形式的类库,比如 BlueBird(可以看作是 Promise 的 polyfill,它是 Promise 被纳入标准前的异步库)。

前两者的问题在于需要开发者自己手动去实现和维护,需要引入新的对象,以及做一些抽象,写成构造函数。而 Promise 是原生的,适合比较简单的异步编程,且流程更加清晰。很多的用到异步的库,如 mongoose,sequlize 等 ORM,都使用 Promise 进行封装(但也支持回调写法)。

参考文章

  1. MDN-使用Promise
  2. MDN-Promise
  3. 掘金-面试精选之Promise
  4. 掘金-Promise 必知必会(十道题
  5. 简书-Promise详解与实现(Promise/A+规范)
  6. JS 异步编程六种方案
  7. Promises/A+规范官方文档
  8. Promise/A+规范中文翻译
  9. ECMAScript 6 入门-Promise 对象