从0.5开始的Promise之旅

236 阅读6分钟

一、为什么要用Promise?

1. JS异步

  • 和同步相对应,同步:会按照代码书写顺序依次执行,后一个任务会等待前一个任务结束,然后再执行;

  • 异步: 代码的执行顺序和书写顺序不一致

    • 异步函数会被放到另一个队列等待结果;其后的函数不必等待异步函数的结果就可以执行,不会被阻塞

      function fn1() {
          console.log('fn1')
        }
      
      function fn2() {
        console.log('fn2')
      }
      
        fn1();
        setTimeout(function () {
          console.log("setTimeout");
        }, 2000);
        fn2();
      
  • 异步操作场景

    • 原生事件处理函数

      • onclick、onmouseover、onkeydown、onkeypress、.....
    • 定时器

      • setTimeout(),setInterval()
    • 发送网络请求

      • 请求API获取数据,数据需要一些时间才能得到,什么时间有返回结果不可预测

        • 不希望发送请求之后,一直等待返回结果,阻塞后面其他的代码执行

2. 在Promise出现之前 JS异步操作是什么样子?

  • Callback回调函数

    • 为了拿到异步执行的返回结果,再执行后续操作,要先定义传入一个函数作为参数,等结果返回后,再回过头调用这个函数,所以叫回调函数。

      function getHotPot(callback) {
          setTimeout(() => {
              callback('火锅')
          }, 2000)
      }
      function printFood(food) {
          console.log(food)
      }
      
      getHotPot(function(data) {
          printFood(data)
      })
      
  • 有什么缺点?

    • 写法复杂,阅读体验不够好

    • 回调地狱

      如果要保证异步操作按顺序执行,就需要层层嵌套
      
      function getBubbleTea(callback) {
          setTimeout(() => {
              callback('奶茶')
          }, 1000)
      }
      
      function getHotPot(callback) {
          setTimeout(() => {
              callback('火锅')
          }, 2000)
      }
      function getFiredChicken(callback) {
          setTimeout(() => {
              callback('炸鸡')
          }, 3000)
      }
      function printFood(food) {
          console.log(food)
      }
      // in order
      getHotPot(function(data) {
          printFood(data)
          getBubbleTea(function(data){
              printFood(data)
              getFiredChicken(function (data) {
                  printFood(data)
              })
          })
      })
      
  • Promise优缺点

    • 优点

      // Promise
      function getBubbleTea() {
          return new Promise((resolve, reject) => {
              setTimeout(() => {
                  resolve('奶茶')
              }, 1000)
          })
      }
      
      function getHotPot() {
          return new Promise((resolve, reject) => {
              setTimeout(() => {
                  resolve('火锅')
              }, 2000)
          })
      }
      
      function getFiredChicken() {
          return new Promise((resolve, reject) => {
              setTimeout(() => {
                  resolve('炸鸡')
              }, 2000)
          })
      }
      
      getHotPot()
          .then(res => {
              console.log(res)
              return getBubbleTea()
          })
          .then(res => {
      				console.log(res)
      				return getFiredChicken()
      		})
      		.then(res => console.log(res))
      
      • 代码更清楚,嵌套关系转变为链式步骤
      • 可读性更好
    • 缺点

      • 无法中途取消,一旦新建就会立即执行

二、Promise是什么?

  • 是一个对象,由关键字 new 及其构造函数来创建的,它承诺在未来的某个时间,一定会返回一个(成功/失败的)结果

    • 3种状态

      • pending 还没有落定(settled),不知道最终结果
      • fulfilled 成功,得到结果
      • rejected 失败,程序出错
      // 1. pending 未落定之前
      function p() {
          return new Promise((resolve, reject) => {
          })
      }
      console.log(p()) // Promise {<pending>}
      
      // 2. fulfilled  期约得到兑现
      function res() {
          return new Promise((resolve, reject) => {
              resolve('一个说话算话的期约')
          })
      }
      
      console.log(res())  // Promise {<fulfilled>: '一个说话算话的期约'}
      
      // 3. rejected  期约失败,未得到兑现
      function rej()  {
          return new Promise((resolve, reject) => {
              reject('失约了')
          })
      } 
      console.log(rej()) // Promise {<rejected>: '失约了'}
      								   // Uncaught (in promise)
      
    • 不可逆

      • 一旦 Promise 落定,它就永远保持在这个状态,不会再更改

      • 原因:

        1. 使用promise代表希望最终得到一个确定的结果(immutable value),可以安全放心地把这个结果传递给第三方,利用这个结果去做后续的事情;
        2. 存在多方调用promise结果的情况,不可逆能够保证任何一方都不能改变这个落定的结果,大家都能拿到同一个确定的结果;
        3. promise的执行从pending落定为fulfilled或rejected需要时间,时间也是不可逆的;

三、Promise怎么用

1. 基础语法 new Promise

function p(flag) {
    return **new Promise**((resolve, reject) => {
        // 参数(接收一个函数作为参数,这个函数的参数是两个函数, resolve、 reject)
        setTimeout(() => {
            if (flag) {
                resolve('fulfilled')
            } else {
                reject('rejected')
            }
        }, 2000)
    })
}

2. 参数

  • 接收一个函数作为参数,这个函数的参数是两个函数(resolve、 reject)
  • resolve在promise成功时调用,将状态从pending兑现为fulfilled;reject在失败时调用,将状态从pending更改为rejected

3. 常用api(then, catch, finally)

  • .then() 接收2个函数作为参数,会在promise状态落定后被调用

    • 第一个函数会在resolved时被调用,能够接收到promise resolved的结果;
    • 第二个函数会在rejected时被调用,接收rejected的结果;
  • .catch() 用于捕获错误

    • 当promise从pending变为rejected时会被调用,相当于.then只传第二个函数作为参数
  • .finally() 在promise结束时,无论结果是fulfilled或者是rejected,都一定会进入finally的回调

    • 通常做一些清理的收尾工作
    function p(flag) {
        return **new Promise**((resolve, reject) => {
            setTimeout(() => {
                if (flag) {
                    resolve('fulfilled')
                } else {
                    reject('rejected')
                }
            }, 2000)
        })
    }
    
    p(true)
        .then(res => console.log(res, '成功!'))
        .catch(error => console.log(error, '出错啦!'))
    		.finally(() => console.log('成功或者失败,最终都会走到这里!'))
    
    // p(true)
    //     .then(res => console.log(res, '成功!'),
    //         error => console.log(error, '出错啦!')
    //     )
    
    项目代码常见用法是在action里,请求数据前开启loading,在finally里取消loading
    const actions = {
        getWarningDetail ({ commit, dispatch }, warningId) {
    	    dispatch('ui/AddLoadingCount', null, { root: true })
    	    return warningAPIs.getWarningDetail(warningId)
    	      .then(res => {
    	        commit(mutationTypes.GET_WARNING_DETAIL, res)
    	      })
    	      .catch(error => {
    	        dispatch(
    	          'ui/showToast',
    	          { text: get(error, 'response.data.errorMessage', '系统错误'), type: '' },
    	          { root: true },
    	        )
    	      })
    	      .finally(() => {
    	        dispatch('ui/SubLoadingCount', null, { root: true })
    	      })
      },
    }
    

4. 链式调用

  • then()的返回结果会被包装成promise对象,具备then方法(thenable),因此可以继续.then(), 连续调用.then()就是链式调用;

  • 前一个then的返回值作为参数传入后一个then

  • 错误处理

    • 链式调用时,把catch放在最后, 一旦某一个then接收到拒绝的结果,则promise 方法会跳过它后面的 then 直接进入catch

      function fetchApi() {
          return new Promise((resolve, reject) => {
                  // resolve('P1')
                  reject('fetchApi error')
          })
      }
      
      function getResult(res) {
          return new Promise((resolve, reject) => {
                 resolve('P2')
              // reject('getResult error')
          })
      }
      
      fetchApi()
      		.then(res => res)
          .then(res => getResult(res))
          .then(lastRes => console.log(lastRes, 'lastRes'))
          .catch(error => console.log(error))
      
    // 如果错误已经被捕获,就不会影响后续的流程
    function fetchApi() {
        return new Promise((resolve, reject) => {
              reject('fetchApi error')
        })
    }
    
    function getResult(res) {
        return new Promise((resolve, reject) => {
             resolve('P2')
        })
    }
    
    fetchApi()
    		.then(res => res)
    		.catch(e => console.log(e, 'caught')) // 处理第一个Promise的错误
    
        .then(res => getResult(res))
        .then(lastRes => console.log(lastRes, 'lastRes'))
        .catch(error => console.log(error, 'last'))
    
    function fetchApi() {
        return new Promise((resolve, reject) => {
              reject('fetchApi error')
        })
    }
    
    function getResult(res) {
        return new Promise((resolve, reject) => {
             resolve('P2')
        })
    }
    // fetchApi失败,不能进getResult,就需要在catch里return Promise.reject
    fetchApi()
    		.then(res => res)
    		.catch(e => {
    				console.log(e, 'caught')
    				return Promise.reject(e)// 立刻返回被拒绝的promise
    			})
        .then(res => getResult(res))
        .then(lastRes => console.log(lastRes, 'lastRes'))
        .catch(error => console.log(error, 'last'))
    

5. Promise.all

  • 参数:一般是由多个Promise对象组成的一个数组

  • 返回:Promise.all返回的是所有promise对象完成状态的结果,会是一个数组,并且返回数据的顺序传入参数数组的顺序对应

  • Promise.all里的多个Promise对象会同时发出请求,等所有promise resolved,才会得到结果

  • Promise.all只要有一个对象rejected,就会进入catch

    function p1() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('p1')
                // reject('p1-error')
            }, 3000)
        })
    }
    function p2() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('p2')
            }, 1000)
        })
    }
    function allP() {
        return Promise.all([p1(), p2()])
    }
    allP()
        .then(res => console.log(res, 'all-res'))
        .catch(error => console.log(error, 'all-error'))
    

6. Promise.race

和Promise.all的区别在于只要有一个对象落定,就直接返回第一个落定的结果

function raceP() {
    return Promise.race([p1(), p2()])
}
raceP()
    .then(res => console.log(res, 'race-res'))
    .catch(error => console.log(error, 'race-error'))