理清 Promise 的状态及使用

理清 Promise 的状态及使用

作者 | 张小虎

杏仁前端开发工程师,前 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 1
const promiseB = promiseA.then(() => {        
    const num = 100 
}) // resolved undefined
const promiseB = promiseA.then(() => {    
    throw 1
}) // rejected 1
const 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 掌握的肯定没问题。

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

// 2
doSomething().then(function () {
  doSomethingElse()
}).then(/* ... */)

// 3
doSomething().then(doSomethingElse()).then(/* ... */)

// 4
doSomething().then(doSomethingElse).then(/* ... */)


全文完


欢迎搜索关注微信公众号:杏仁技术站(微信号 xingren-tech)。


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

发布于 2018-07-16 10:30

文章被以下专栏收录