Promise 與 Async/Await 的異步之旅

Prommise

簡介

回想一下我們使用傳統方式(回調函數)是如何來解決異步編程依賴問題的

login(param, function() {
  consloe.log('登錄成功,開始獲取用戶信息')
  getUser(param, function() {
    console.log('已經獲取到用戶信息,現在來獲取菜單')
    getMenu(param, function() {
      console.log('獲取到菜單了')
    })
  })
})

恐怖嗎?這還只是三層依賴,如果有更深的依賴關係,那真的會陷入 ajax 的回調地獄。爲了解決這個問題, 晴空一聲霹靂響,天上掉下個 Promise

Promise 是異步編程的一種解決方案,ES6 將其寫入了語言標準。譯爲 “承諾”,你將一件事情交給他管理,等到這個事情有了結果,或者成功或者失敗,它會承諾給你一個結果。

Promise 可以理解爲一個容器,它肚子裏裝着一個函數,通常在這個函數裏進行一些異步請求,會在將來某個時候拿到結果。Promsie 對象具有以下兩個特點:

  • 三個狀態:pending(進行中)、fullfiled(已成功)、rejected(已失敗),狀態只能在異步函數中改變,外界無法改變其狀態
  • 狀態不可逆:狀態只能從 pending -> fullfiled 或 pending -> rejected

基本用法

先介紹下 Promise 的基本用法,ES6 規定,Promise 是一個構造函數,使用生成 Promise 實例。

var login = function (param) {
  return new Promise(function (resolve, reject) {
    ajax('http://127.0.0.1/login', function(rest) {
      if (rest.success) {
        resolve(rest)
      } else {
        reject(new Error('登錄失敗'))
      }
    })
  })
}

構造函數 Promise 接收一個函數作爲入參,該函數接收 resolve、reject 兩個參數,這兩個參數會由 js 引擎提供,無需自己傳入

  1. 當異步函數判斷結果爲成功時調用 resolve,這時 Promise 的狀態就會從 pending 變成 fullfiled
  2. 當異步函數判斷結果爲失敗時調用reject,這時 Promise 的狀態就會從 pending 變成 rejected

現在我們用 Promise 把 login 做了改造,return 了一個 Promise 實例。那麼這個 login 該怎麼用呢?

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}, function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
})

login 函數執行會返回一個 Promise 對象,Promise 對象上有個 then 方法,它接受兩個函數

  • 前者爲異步成功時的回調,暫時可以理解爲註冊了 resolve 函數
  • 後者爲異步失敗時的回調,暫時可以理解爲註冊了 reject 函數

reject 函數除了可以註冊在 then 函數的第二個參數上以外,還可以註冊在 Promise.prototype.catch 函數中

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
})

不管成功或者失敗,我都想執行一步操作怎麼辦呢?ES9 引入了 Promise.prototype.finally 函數

login(param).then(function (rest) {
  // 當執行 resolve 時就會進入這裏,表示成功,rest就是後端返回的登陸信息
}).catch(function (e) {
  // 當執行 reject 時就會進入這裏,表示失敗
}).finally(function () {
  // 不管成功或者失敗,都會走到這裏
})

鏈式調用

咋一看,用了 Promise 之後代碼量還增加了一點對吧!那它帶來的好處是什麼呢?別急,接着往下看

當我們把另外兩個接口 getUser、getMenu 兩個接口都用 Promise 改造之後......

login(param)
.then(function(loginData) {
    return getUser(loginData)
}).then(funtion(userData) {
    return getMenu(userData)
}).then(function(menuData) {
    // 可以一直鏈下去
})

 我們跳出了嵌套地獄,函數不用再寫在回調裏了,只需要使用 Promise 提供的 then 函數把所以的依賴按順序給鏈接起來

  • 前一個異步執行結束之後纔會執行下一個異步
  • 前一個 then 裏面的異步執行結果會傳入到下一個 then 註冊的函數裏,如上述代碼中 getUser 的結果會傳給 getMenu 這一行

Promise.all

上面是鏈式串行調用的方式,各個函數之間是有依賴關係的。但是現在我有這個需求

  • 異步函數之間沒有依賴關係
  • 我想要它們並行的去執行,這樣會比串行快很多
  • 等到所有的函數都執行完了把結果一起返回給我

這個時候 Promise.all 就排上用場了,現在假設上文中的 login、getUser、getMenu 三個函數之後沒有依賴關係

Promise.all([login(), getUser(), getMenu()]).then(function(rest) {
    // 三個異步函數都執行完了纔會走到這裏
    console.log(rest); // [loginData, userData, menuData]
}).catch(function(e) {
    // 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})

Promise.all 函數的入參是一個數組,數組中的每個元素都是一個 promise 實例,經過 Promise.all 封裝成了一個新的 promise 實例。等到數組中所有 promise 實例的狀態都變成 resolved 的時候就會,這個新的 promise 實例的狀態頁面變成 resolved。如果數組中有一個 promise 的狀態變成了 rejected,那麼這個新的 promise 的狀態也就變成了 rejected。

Promise.race

現在我的需求又變了,這三個異步函數誰先執行完我就先處理誰,其他兩個我就不管了。這個時候就得用 Promise.race 了,我們稱之爲“競賽模式”。

Promise.race([login(), getUser(), getMenu()]).then(function(rest) {
    // 只要有一個先執行結束,它的結果就會返回到 rest 上
    console.log(rest); // loginData 或 userData 或 menuData
}).catch(function(e) {
    // 只要有一個函數失敗,執行了 reject,就會走到這裏,promise 結束
})

自己擼一個

現在都講究造輪子,既然 Promise 的用法我們都已經知道了,那就自己來擼一個吧!

從用法上我們可以看出一下幾點

  • Promise 是一個構造函數
  • 有一個狀態
  • 有兩個數組,一個用來保存成功時的回調函數,一個用來保存失敗時的回調函數
  • 原型上有 then、catch、finally 這麼幾個方法
  • 有 resolve、reject、all、reace 這麼幾個靜態方法

構造函數

function Promise (executor) {
  var self = this;
  this.status = 'pending';  // 狀態
  this.data = undefined;
  this.onResolvedCallback = []; // 通過 then 註冊的成功回調
  this.onRejectedCallback = []; // 通過 then 或者 catch 註冊的失敗回調
  
  function resolve (value) {
    if (this.status === 'pending') {
      self.status = 'resolved'; // 成功時將狀態改爲 resolved
      self.data = value;
      for (var i=0; i<this.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value) // 執行註冊的成功函數
      }
    }
  }

  function reject (reason) {
    if (this.status === 'pending') {
      self.status = 'rejected'; // 失敗時將狀態改爲 rejected
      for (var i=0; i<this.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason) // 執行註冊的失敗函數
      }
    }
  }

  try {
    executor(resolve, reject);  // new Promise 的時候立即執行 executor
  } catch (e) {
    reject(e)
  }
}

then 

then 函數是用來註冊成功或者失敗時的回調函數的。需要注意的是,當調用 then 函數來註冊回調時,promise 實例的狀態有可能是 pending、resolved、rejected。

Promise.prototype.then = function (onResolved, onRejected) {
  // 爲了可以使用鏈式寫法,then 方法返回的是一個 promise 對象
  if (self.status === 'pending') {
    // 通常情況下,如果 promise 包裹的是一個異步操作,那麼走到 then 是,promise 應該還是 pending狀態
    return promise1 = new Promise(function (resolve, reject) {
      // 註冊成功回調
      // self.onResolveCallback.push(onResolved)
      // 本來我們直接把 onResolved push 到 onResolveCallback 這個數組裏就可以了,異步執行完成時會調用到這個 onResolved,並把結果傳進去
      // 但是 onResolved 裏面可能還有一個異步操作(假設p),我們得等到這個異步p的狀態改變了之後,才能繼續下一個 then,這樣才能完成正確的依賴關係
      // 所以我們還得判斷 onResolved 函數的返回結果
      self.onResolveCallback.push(function (value) {
        var x = onResolved(value);
        if (x instanceof Promise) {
          // 如果 onResolved 返回的仍是一個 promise,那就等等到這個 promise 的狀態改變了之後,才能改變 promise1 的狀態,然後繼續下一個 then
          x.then(resolve, reject)
        } else {
          // 否則直接改變 promise1 的狀態,把 promise1 的執行結果 x 傳入到下一個 then 中的回調
          resolve(x);
        }

      })

      // 註冊失敗回調
      self.onRejectCallback.push(function(reason) {
        var x = onRejected(reason);
        if (x instanceof Promise) {
          x.then(resolve, reject)
        } else {
          reject(reason)
        }
      })
    })
    
    
  }
  if (self.status === 'resolved') {
    // 如果 new Promise(executor) 的時候,傳入的 executor 內並沒有執行異步操作,而是直接調用了 resolve,那麼走到 then 的時候,Promise 已經是 resolved 狀態了
    // 爲了能夠鏈式調用我們還是要返回一個 promise
    return new Promise(function(resolve, reject) {
      // 由於狀態已經是 resolved 了,就不需要把 onResolved、onRejected push 到隊列中了
      // 直接把 executor 的執行直接塞進 onResolved 就行了
      var x = onResolved(self.value)
      if (x instanceof Promise) {
        x.then(resolve, reject)
      } else {
        resolve(x)
      }
    })
  }
  if (self.status === 'rejected') {
    // 邏輯和上面差不多,只是最後一步應該執行 reject(x)
  }
}

all

做一個計數,等所有 promise 都完成了,執行 resolve 改變狀態就行了

Promise.all = function(arr) {
  return new Promise(function (resolve, reject) {
    var count = 0;  // 記錄已完成了 promise
    var list = []
    // 數組 arr 中每一項都是一個 promis 對象
    arr.forEach((p,index) => {
      p.then(res => {
        count ++;
        list[index] = res;
        if (count === arr.length) {
          // 所有的 promise 都成功了
          resolve(list)
        }
      }).catch(e => {
        reject(e)
      })
    })
  })
}

race

做一個標識,第一個完成的 promise 執行 resolve 改變下狀態,其他的就不管了

Promise.race = function(arr) {
  return new Promise(function (resolve, reject) {
    // 做一個標識,如果有一個完成了執行 resolve,等其他的 promise 完成了就不管了
    var done = false;
    // 數組 arr 中每一項都是一個 promis 對象
    arr.forEach((p,index) => {
      p.then(res => {
        if (!done) {
          resolve(res)
        }
      }).catch(e => {
        if (!done) {
          reject(e)
        }
      })
    })
  })
}

 重要的就這麼幾個函數,好了,Promise 的使用和實現到此結束。

然而技術總是在不斷的更新,程序員在追求代碼優雅的路上也在不斷的探索。

Async/Await

簡介

async 函數在 ES2017 納入了標準,它是 Generator 函數的語法糖,使得異步操作變得更加簡單。

基本用法

async 函數的返回值是一個 promise,可以使用 then 註冊回調函數。它和 await 搭配使用,當 async 函數執行到 await 的時候,就會等待 await 後的函數執行完之後再執行後面的內容。比如文章開頭的三個函數就可以寫成這樣

async function start () {
  var loginData = await login().catch(...);
  var userData = await getUser();
  var menuData = await getMenu();
  return menuData;
}
start.then(...)

看起來是不是很簡單、很優雅,我們可以像寫同步代碼一樣去操作異步函數。而且比起 Promise 我們可以更加容易的去中斷鏈路,在任意一行 return 就行了。

這裏需要注意幾點

  • async 必須緊跟着 function,像這種寫法是錯誤的 var async start = function () {}
  • await 必須在 async 申明的函數內部使用,不然會報錯
  • await 後面需要是一個 promise 對象,才能達到“等待”的效果。其他對象是達不到這個效果的,如 await setTimeout()

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章