理清 Promise 的状态及使用

1,008 阅读7分钟
原文链接: mp.weixin.qq.com

作者 | 张小虎

杏仁前端开发工程师,前 iOS 开发工程师,关注前端技术栈。

为什么会有 promise?

因为要解决回调函数的嵌套,也就是所谓的回调地狱,回调地狱长啥样大家应该有点数吧?

doSomethingA((res) =>{if (res.data) {
    doSomethingB(res.data, (resB) => {
      if (resB.data) {
        doSomethingC(resB.data)
      }
    })
  }})

这样的代码不太美观,还得依靠缩进来分清层级。那解决这种回调地狱的方式有很多,最简单方式是定义好一堆具名函数直接调用。那进阶一点方式便是 promise。

promise 是什么?

通过 Promise 构造函数可以创建 promise 对象,promise 是一种通过链式调用的方式来解决回调函数传递的异步方案。

promise 对象具有状态,pending、fulfilled、rejected。状态改变之后就不会再变化。

promise 实例

通过 new 关键字初始化得到新的 promsie 对象。

const promise = new Promise(function(resolve, reject) {// ... do something
    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }})

promise 对象创建便会立即执行,但 promise 会保存状态。

基本用法

Promise 定义了一些原型方法来进行状态处理。最常用的有:

  • Promise.prototype.then

const pm = new Promise(function(resolve) {setTimeout(function() {
        resolve(100)
    }, 2000)})

pormise 对象通过内部 resolve 函数完成状态的 pending -> fulfilled。此后这个promise 将保留这个状态。可以通过 then 方法去处理状态。

pm.then((val) => {    console.log(val); // 100})

then 方法也可以用来处理 rejected 状态,then 方法可以接收 2 个函数参数。

new Promise(function( resolve, reject ) {    setTimeout(() => {
        reject(new Error('err'))
    }, 2000)}).then(null, (error) => {
    console.log(error); // error: err})

但用 then 的第二个参数去处理错误不是最好的选择。因为大多数情况下我们会用到链式调用。类似:promise.then().then().then()。所以在每个 then 方法去处理错误显得代码很多余,而且也真的没必要。

  • Promise.prototype.catch

catch 方法就是用来做错误统一处理。这样链式调用中我们只需要用 then 来处理 fulfilled 状态,在链的末尾加上 catch 来统一处理错误。

new Promise((resolve, reject) => {setTimeout(() => {
        resolve(100);
    }, 2000)}).then(result => {
    return result * num // 这里模拟一个错误,num 未定义}).then(result => {
    return result / 2;}).catch(err => {
    console.log(err); // num is not defined})

这里举得例子比较简单,在 then 方法里面没有去 return 新的 promise。可以看到第一个 then 发生了错误,最后的 catch 会捕捉这个错误。catch 实际上是.then(null, rejection)的别名。

  • Promise.prototype.finally()

这个 api 一般用来放在结尾,因为它不管前面的 promise 变为什么,它都会执行里面的回调函数。

new Promise((resolve, reject) => {    setTimeout(() => {
        resolve(100);
    }, 2000)}).then(result => {
    return result * num // 这里模拟一个错误,num 未定义}).catch(err => {
    console.log(err); // num is not defined}).finally(() => {
   console.log('complete'); // complete })

这便是一个完整的状态处理流程,then() 处理 fulfilled、catch() 处理 rejected、finally 两种都处理。但 finally 目前还只是提案阶段。

  • Promise.all

这个 api 在开发中也比较实用,如果 C 行为依赖于 A、B 行为,A、B 之间又没有依赖关系,而且只有当 A、B 的状态都为 fulfilled,或者有一个变为 rejected 时才会开始 C 行为。这时用 Promise.all() 就显得比较合适。

Promise.all([A, B]).then((resA, resB) => {    ... // do something}).catch(() => {
    ... // do something})

如果不用 Promise.all 也能做到 A、B 完成之后再调用 C,一般会这么写:

promsiseA.then(() => {    return promsiseB;}).then(() => {
    ... // do something}).catch(() => {
    // do something})

这样就好比强行将 A、B 置为串行,所以对比 Promise.all() 显得效率不高。

  • Promise.race

用法和 Promise.all 一样,C 依赖于 A、B,但只要 A,B 的任一状态改变了,C 便开始执行。

Promise.race([A, B]).then((res) => {    ... // do something}).catch(() => {
    ... // do something})

链式调用

之所以能在 promise 的使用中进行链式调用,是因为 then、catch、finally 方法都会返回新的 promise 对象。链式调用在 js 中也很常见。

[1, 2, 3].map(num => num*2).join('-')  // "2-4-6"$('#box').find('.info').text('hello world') // jquery 链式

其实链式调用原理都一样,让方法始终返回新的对象就好了。

promise 中的 then、catch、finally 方法也是如此,总是返回新的 promise。这里都能理解,但有趣的是 promise 具有了状态。这让链式变得稍微复杂了些。

  • 状态变化

then、catch、finally 返回 promise 但这些 promise 的状态由谁决定呢?

答案是如果处理了状态那新得到的 promise 的状态由处理函数的具体内容决定,如果没处理状态那得到 promise 的状态直接继承前面 promise 的状态。

假设 promiseA 的状态为 resolved

const promiseB = promiseA.then(() => {    return 1}) // resolved 1const promiseB = promiseA.then(() => {
    const num = 100 }) // resolved undefinedconst promiseB = promiseA.then(() => {
    throw 1}) // rejected 1const promiseB = promiseA.then(() => {
    return promiseC}) // 状态由 promiseC 决定

假设 promiseA 的状态为 rejected

const promiseB = promiseA.then(() => {    // do anything }) // 状态沿用 promiseA: rejected
  • 错误处理

const pm = new Promise((resolve, reject) => {
    reject('err') // 直接将 pm 对象的状态变为 rejected})var pm1 = pm.then(result => {
  console.log('hello, then 1');  // 不会执行
  return 100; // 不会执行});

这里由于pm 的状态是 rejected, 所以 .then 继续将 rejected 状态向下传递,这样我们就能通过末尾的 catch 操作处理异常。

pm.then(result => {
    return result * 2}) // rejected.then(result => {
    return result * 4}) // rejected.catch(err => {
    console.log(err)})

配合 async await 使用

如果能比较熟练的使用 promsie 再过渡到 async、await 也没什么问题。

  • 怎么使用

async function f() {    await 100;}f().then((num) => {
    console.log(num); // 100})

简单的例子似乎看不出 async await 到底是干什么的,又有什么用。但可以知道 async 返回的是 promise, await 相当于改变状态的操作。

  • 对比 promise 优势在哪?

aysnc 、await 最大的好处是简化 promise 的 .then 操作,让代码更加同步。

pmA.then((dataA) => {    ...
    return dataB}).then((dataB) => {
    ...
    return dataC}).then((dataC) => {
    ...
    console.log('end')})

换做是 async、await 就可以写成

async f() {    const dataA = await pmA;
    const dataB = await fb(dataA);
    const dataC = await fc(dataB);
    ...}

所以不管 await 后面去接什么样的异步操作,整体上还是会保持同步的方式,这样看起来结构清晰。

总结

从 promsie 到 async、await,JS 提供的这些新的特性,都让 JS 的使用体验变得越来越灵活方便。从 ES6 的出现到现在的 ES7、ES8,也都能感受到 JS 在拥有更大的能力去做好更多的事情。关于 promise 的使用还是要多用多体会,不然用 promise 写出回调地狱一样的代码也不是没有可能。最后还是贴出经典的 promise 问答题!据说能回答正确这些写法区别的 promise 掌握的肯定没问题。

// 1doSomething().then(function () {
  return doSomethingElse()}).then(/* ... */)// 2doSomething().then(function () {
  doSomethingElse()}).then(/* ... */)// 3doSomething().then(doSomethingElse()).then(/* ... */)// 4doSomething().then(doSomethingElse).then(/* ... */)

以下文章您可能也会感兴趣:

我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

杏仁技术站

长按左侧二维码关注我们,这里有一群热血青年期待着与您相会。