Promise 的理解和应用

一、为什么要有Promise

在研究 Promise 之前我们先来看一段Jquery ajax 的操作

// 为方便表述我们给这段代码取名为 ajaxA 
$.post('/your/api/a',{'paramKey':'value'},function(data,status){
  if('success' == status){
      console.log(data);//获取到数据
  }
})

以上代码用ajax对服务端发起了请求并获取返回的数据 data,网络请求需要一定时长,所以我们ajax请求的回调中处理数据。如上述 console.log(data)。如果感觉还不够直观,请看下面的错误示范

// 为方便表述我们称下面这段代码为 代码A 
let targetData = 1;
 // setTimeout 模拟 ajax 的请求延迟
 let t = setTimeout(() => {
         clearTimeout(t);
         targetData = 10;
 }, 1000);
 console.log(targetData);//1

以上代码直观表示只能在回调中处理数据。那么假设我们的 ajaxA 返回的数据,又得继续当作 ajaxB 的参数再一次请求数据。那么代码将会变成以下这样子

// ajaxA
$.post('/your/api/a',{'paramKey':'value'},function(data,status){
  if('success' == status){
      console.log(data);//获取到数据
    // ajaxB 以 ajaxA 返回的数据为参数
    $.post('/your/api/b',data,function(data,status){
      if('success' == status){
        console.log(data);//获取到数据
        //假设还有,那就俄罗斯套娃继续往下套
      }
    })//END ajaxB
  }
})

这才是两层的代码,就已经显得十分混乱了,当然你可以封装函数取调用,但是治标不治本,依然混乱。我们暂且极端地假设有10层或者更多层回调,首先代码的可阅读性就会变得很差,其次这多层中某一层出问题或者失败都需要有不同的处理,因此十分混乱,简直就是地狱了。这就是JS中的“回调地狱”。

二、Promise 怎么解决回调地狱

如何优雅解决上述问题呢,ES6中提供了 Promise,Promise 的出现使得我们能像同步任务一样来编写异步任务的代码。我们先来改写一下“代码A”

let targetData = 1;

//把setTimeout定义在 new promise 中
let promise = new Promise((resolve, reject) => {
    let t = setTimeout(() => {
        clearTimeout(t);
        targetData = 10;
        resolve();//将Promise 的状态改为成功 fulfilled 
    }, 1000);
})

//在 promise.then 获取 targetData
promise.then(()=>{
    console.log('targetData',targetData)
})

这就特别像是在写同步任务的代码了。我们来写段代码演示多层回调有什么区别

function promisePost(url, data) {
    return new Promise((resolve, reject) => {
        let t = setTimeout(function () {
            clearTimeout(t);
            //假设全是成功的
            resolve(data);
        }, 200)
    })
}

//请求A
promisePost('url A', 'data A').then(res => {
    console.log(res);
    return promisePost('url B', 'data B');//为方便表述取名为 promiseB
}).then(res => { //注意这里的 then 是 promiseB 的
    console.log(res);
    return promisePost('url C', 'data C');//为方便表述取名为 promiseC
}).then(res => { //注意这里的 then 是 promiseC 的
    console.log(res);
    return promisePost('url D', 'data D');//为方便表述取名为 promiseD
}).then(res => {//注意这里的 then 是 promiseC 的
    console.log(res);
})

打印结果如下

data A
data B
data C
data D

链式调用就比嵌套更具有条理性了,代码的可阅读性也得到提高。

三、Promise 如何捕获异常

上面代码我们有多层回调依赖,假如其中某一环出现错误,该怎么处理。

我们先改造 promisePost 代码

function promisePost(url, data) {
    return new Promise((resolve, reject) => {
        let t = setTimeout(function () {
            clearTimeout(t);
            //模拟成功或者失败
            Math.random() > 0.5 ? resolve(data) : reject('失败的URL: ' + url);
        }, 200)
    })
}

然后在链式调用的最后添加一个catch

//请求A
promisePost('url A', 'data A').then(res => {
    console.log(res);
    return promisePost('url B', 'data B');//为方便表述取名为 promiseB
}).then(res => { //注意这里的 then 是 promiseB 的
    console.log(res);
    return promisePost('url C', 'data C');//为方便表述取名为 promiseC
}).then(res => { //注意这里的 then 是 promiseC 的
    console.log(res);
    return promisePost('url D', 'data D');//为方便表述取名为 promiseD
}).then(res => {//注意这里的 then 是 promiseC 的
    console.log(res);
}).catch(err => {
  console.log(err);
})

取其中一次打印结果举例

data A
data B
失败的URL: url C

因为是随机模拟成功或者失败,所以多刷新几次会发现规律,链式调用中遇到失败的promise 则不会继续调用下一个 promise 的 then,catch 只需要在链式的最后添加,利用错误冒泡机制一层一层往上传,直到被catch捕获。不得不说确实优雅!

四、Promise 的其他补充
Promise 的 三种状态

pending:进行中,表示Promise还在执行阶段

fulfilled:成功状态,表示 promise 成功执行

rejected:拒绝状态,表示promise被拒绝,或者说失败

promise 只有能是其中一种状态,可以从pending变成其他两种状态,之后状态就会固定,无法在次发生更改。

  • 在执行 resolve 时,会将Promise 的状态改为 fulfilled,并执行then方法。resolve 可以携带参数,参数将作为返回值返回。
  • 在执行 reject 时,会将Promise的状态改为 rejected,并会被 catch 捕获 reject 可以携带参数,参数将作为返回值返回。

除了promise.then 和 promise.catch 外还有 promise.finally。finally 无论 promise 的状态是 fulfilled 还是 rejected,到最后都会执行。可以在这里做一些资源回收或者统一业务的处理。

Promise.all 方法

Promise.al提供了并行执行异步任务的能力并且会统一返回结果,每个结果为数组的一个元素

Promise.all([
    new Promise((resolve,reject)=>{
       resolve('result A')
    }),
    new Promise((resolve,reject)=>{
       resolve('result B')
    })
]).then(res=>{
    console.log(res)// ['result A', 'result B']
})

假如有时间差,结果会不会不同,改造代码如下

Promise.all([
    new Promise((resolve, reject) => {
        let t = setTimeout(() => {
            clearTimeout(t)
            resolve('result A')
        }, 500);//慢
    }),
    new Promise((resolve, reject) => {

        let t = setTimeout(() => {
            clearTimeout(t)
            resolve('result B')
        }, 100);//快
    })
]).then(res => {
    console.log(res)// ['result A', 'result B']
})

结果依然是 [‘result A’, ‘result B’] ,结果有序,利用这个特征就可以做一些有顺序的结果处理。

Promise.race 方法

与 all 返回结果不同的是race只返回最快的 primise 的结果

Promise.race([
    new Promise((resolve, reject) => {
        let t = setTimeout(() => {
            clearTimeout(t)
            resolve('result A')
        }, 1000);
    }),
    new Promise((resolve, reject) => {

        let t = setTimeout(() => {
            clearTimeout(t)
            resolve('result B')
        }, 100);
    })
]).then(res => {
    console.log(res)//result B
})
Promise 嵌套语法糖

来看两段代码:

代码一

let promise = new Promise((resolve, reject) => {
    resolve(['第一个'])
}).then(res => {
    res.push('第二个')
    return new Promise((resolve, reject) => {
        resolve(res)
    })
}).then(res => {
    res.push('第三个')
    return new Promise((resolve, reject) => {
        resolve(res)
    })
}).then(res => {
    console.log(res)
})

输出结果为:[‘第一个’, ‘第二个’, ‘第三个’]

代码二

let promise = new Promise((resolve, reject) => {
    resolve(['第一个'])
}).then(res => {
    res.push('第二个')
    return res;
}).then(res => {
    res.push('第三个')
    return res;
}).then(res => {
    console.log(res)
})

输出结果为:[‘第一个’, ‘第二个’, ‘第三个’]

以上两段代码是等价的,在Promise 嵌套中关于 new promise 部分的代码可以不用写,简洁非常多。但是代码的直观性上差了些,用不用这个语法糖就看个人喜好了

未经允许不得转载