從 Promises/A+ 看異步流控制

同步發表於 個人博客html

單線程的挑戰

衆所周知,JavaScript 是一種 單線程 編程語言。單線程註定了在當前執行上下文棧中的代碼尚未執行完成時,後續全部 task/micro-task queue 中的 task/micro-task 代碼都不會執行。前端

爲何要單線程,多線程很差嗎?git

JavaScript 設計之初,它的設計定位是一種主要用於操做 DOM 的圖靈完備語言。爲了不併發操做 DOM 帶來的併發操做問題,並結合其語言定位,就註定了它單線程的命運。github

Ajax 出現以前,全部的 JS 代碼都是以同步的方式逐步執行。Ajax 的出現爲網頁帶來了在不刷新頁面的前提下就能更新頁面的能力。Web 世界在搭上了 Ajax 的快車以後飛速發展。可是,當網絡條件不穩定時問題出現了,同步的 Ajax 會始終佔用主線程的執行上下文,致使在請求沒有響應的期間,全部後續的同步代碼都沒法執行,那麼整個網頁就陷入了 「癱瘓」,全部的用戶交互都被堵在了 task queue 中。web

用戶交互屬於 task 而非 micro-task。編程

這個時候聰明的人們發明了 異步請求。在每次請求發出後,設置一個回調函數等待響應返回再處理響應,而且在等待的過程當中 再也不佔用當前執行上下文設計模式

然而事情並無那麼簡單。隨着時間推移,網頁需求愈來愈複雜,網頁應用也愈來愈複雜,一不當心咱們就掉進了 callback hell,究其緣由是咱們沒有一個優雅的前端異步流控制 asynchronous flow control 解決方案。api

以下:promise

const verifyUser = function(username, password, callback) {
  effect.verifyUser(username, password, (error, userInfo) => {
    if (error) {
      callback(error)
    } else {
      effect.getRoles(username, (error, roles) => {
        if (error) {
          callback(error)
        } else {
          effect.logAccess(username, error => {
            if (error) {
              callback(error)
            } else {
              callback(null, userInfo, roles)
            }
          })
        }
      })
    }
  })
}
複製代碼

常規的 callback API 再也不可以知足包含大量異步操做的網頁應用,大量的 異步操做 就意味着必須有一個合理的 異步流控制 方案來掌控各類異步流操做。瀏覽器

那麼後來咱們是如何避免回調地獄來清晰地實現異步流控制?

答案是 Promises/A+ 規範。

本文將嚴格依據 Promises/A+ 規範約束最終實現一個自定義版本且經過 規範測試套件 的異步流控制解決方案。在 @lbwa/promise-then 你能夠找到完整的 Promises/A+ 實現。

什麼是 Promises/A+

Promises/A+ 一個略微中二的名字,但它給社區給開發者帶來了異步操做流的重磅炸彈—— Promise。 規範定義了一個名爲 Promise 的對象來承載當下異步操做流的 最終結果。它用同步的方式表示了當下異步操做流的狀態以及與狀態相關聯的返回值。

  1. 在操做成功的狀況下,異步操做的返回值 value 是多少;

  2. 在操做失敗的狀況下,又是由於什麼緣由 reason 失敗的。

// 僅藉助 ts 的接口來表示 Promise 的 **抽象** 結構,不表明實現細節
interface Promise {
  state: PromiseState
  value: any // 不管是成功仍是失敗,都經過該字段存儲對應的值或失敗緣由
}
複製代碼

Promise 對象交互的 首要方式 都是經過一個名爲 then 的方法來實現,如修改 Promise 的狀態,返回異步操做返回值或異步操做失敗緣由。

那麼在定義了這樣的規範核心的前提下,全部的規範內容都是圍繞實現以上兩點核心功能點。

如何表示異步流的當下狀態

根據上文中 Promises/A+ 規範對 Promise 對象的定義。從本質上來說,Promise 是一種抽象的狀態模型,一種 有限狀態機。規範在第一章首要位置首先定義了 3 個值來表示 Promise 對象的狀態,即表示了全部異步操做流可能出現的三種狀態:

  1. pending 狀態,如字面意思同樣,表示當下異步操做流正在執行中,正在 等待 異步流操做的結果。

  2. fulfill 狀態,表示當下異步操做流已經操做 成功,並在 Promise 對象中包含了當下操做的執行結果。

    本文遵循 Promise/A+ 的表示方式,fulfill 等價於 ES6 Promise 中的 resolve 狀態。

    interface FulfilledPromise {
      state: States.fulfilled
      value: any
    }
    複製代碼
  3. reject 狀態,表示當下異步操做流操做 失敗,並在 Promise 對象中包含了當前操做失敗的緣由 reason

    interface RejectedPromise {
      state: States.rejected
      value: Error
    }
    複製代碼

全部的異步操做流僅存在以上三種操做狀態,那麼筆者藉助 TypeScript 中的靜態枚舉可實現如下結構包含全部的靜態狀態變量值:

const enum States {
  pending = 'pending',
  fulfilled = 'fulfilled',
  rejected = 'rejected'
}
複製代碼

在筆者我的對 Promise/A+ 的實現中,之因此使用靜態枚舉的緣由是在 TypeScript 中全部的靜態枚舉值均可在 compile time 時期被直接編譯爲 靜態變量字面量,而不是 JS 運行時中的一個樸素的 JS 對象,這裏的靜態枚舉起到的做用相似於一些編譯型語言(如 C++)中的 編譯時常量

實現的技術核心是什麼

不管是回調地獄仍是 Promises/A+ 規範,首先拋開實現技術細節上來看,全部的異步操做流程都具備:

  1. 註冊回調

    由於是異步流控制,異步流內部相對於外部模塊來講始終是在進行異步操做,那麼在執行異步操做開始,進行中,結束時的任意階段都首先經過異步操做模塊對外暴露接口 註冊一個或多個回調函數。

    回調地獄只能在異步流開始以前進行回調註冊,而 Promises/A+ 定義了同一個 Promise 實例能夠調用屢次 then 函數,即實現了多階段多個回調註冊。

  2. 觸發回調

    在異步流再也不爲 pending 狀態時,那麼給對應狀態註冊的全部回調函數,會 依據註冊順序 分別獲得執行。

經過對關鍵點的梳理,全部的回調函數都是直接依賴於異步流的狀態的,那麼它們都是當前異步流狀態變化的 觀察者,而當前異步流的狀態變化始終是全部回調函數的 主題。結合兩者的關係分析,不難經過 observer 模式實現異步流控制的 技術核心

這裏筆者的實現是將 then 回調傳入的回調函數 onFulFilledonRejected 合二爲一看待,它們造成一個 回調函數 總體 ThenableCallbacks。該集合總體 直接依賴 於當前的 Promise 實例,當前實例做爲全部回調集合主題 subject。側重於集合總體觀察 Promise 狀態變換,而非具體狀態值。在存在 Promise 狀態值變換,即 subject 變化時,將廣播至每個回調集合。故說是經過 observer 模式(而不是 publish/subscribe 模式)。

interface ThenableCallbacks {
  onFulfilled?: OnfulfilledCallback
  onRejected?: OnRejectedCallback
}
複製代碼
ThenableCallbacks --觀察--> Promise.state 的變化
複製代碼

固然存在另一種抽象思路是,將 onFulfilledonRejected 都認爲是獨立的個體,它們分別經過 fulfilledQueuerejectedQueue間接依賴 當前的 Promise 實例。而且 fulfilledQueuerejectedQueue 經過 message brokerevent bus 來訂閱各自的指望主題 topic。這個時候整個模式側重的再也不是 Promise 實例的狀態變化,而是實例的某一個具體的狀態值。全部的回調函數的觸發都是經過 message brokerevent bus 來發布對應的 topic 來實現函數調用。而這種抽象設計模式正是 publish/subscribe 模式。

type FulFilledQueue = OnfulfilledCallback[]
type RejectedQueue = OnRejectedCallback[]
複製代碼
onFulfilled --註冊--> fulfilledQueue --訂閱--> fulfilled state
onRejected --註冊--> rejectedQueue --訂閱--> rejected state
複製代碼

這裏兩者設計模式實現的異步流控制方案複雜程度並無太大差別,本文也將採用第一種 observer 方式來嚴格實現技術細節。

異步控制流的初始化

在初始化異步流階段,咱們應該如何實現 Promise 初始化?經過對規範中 The promise resolution procedure 閱讀可見,規範中將異步流初始化定義爲一種基於單個 promise 和一個單值 x 處理的異步流抽象操做,記爲 [[Resolve]](promise, x)

  1. 這裏 promise 就是在初始化 Promise 構造函數所返回的 Promise 實例,記爲 $0。它始終是對外暴露的。

  2. 單值 x 是做爲當前異步流的結果載體,它自己是表示一個異步流的結果返回值,不該被模塊外部直接修改。當 x 表示一個非 Promise 實例的值時,它的存在會直接被當前 $0 實例直接引用爲 Promise 的實例 value 字段的值。

    細心的讀者可能發現,x 值是能夠爲另外一個 Promise 實例的,那麼在這種狀況下,當前 $0 實例會嘗試直接同步 x 變量所引用的 Promise 實例的狀態 state,並將該實例的 value 字段的異步流結果值,直接賦值給 $0 實例的 value 字段。

promise procedure

在基於以上兩點的前提下,咱們在初始化 Promise 階段的核心目標是,實現一個有限狀態機,並接收一個 executor接收 狀態機對外的暴露的狀態變換器。該變換器的核心目標是,在被調用的狀況下,修改狀態機內部的狀態值。另外,基於 Promises/A+ 規範中的定義,全部已經固定的狀態,沒法被再次修改

Promise 實例的狀態被狀態變換器觸發改變的條件下,同時經過狀態變換器來接收執行結果或執行錯誤。並將執行結果或執行錯誤賦值給 Promisevalue 字段,以供 Promise 的交互接口 then 函數來獲取對應的異步流操做狀態和操做結果。

class Promise {
  // 初始化實例的狀態爲 pending 狀態
  private state: States = States.pending
  private value: any = undefined

  // 模塊內部的狀態變換器核心
  private _settle(state: Exclude<States, States.pending>, result: any) {}

  constructor(executor: (onFulfilled, onRejected) => void) {
    executor(
      function fulfill(result?: any) {}, // 對外狀態變換器
      function reject(reason?: Error) {} // 對外狀態變換器
    )
  }
}
複製代碼

在上文中,經過簡單代碼展現了 Promise 初始化的核心抽象流程。fulfill 函數對應了前文所述的向模塊外部暴露,用於修改 Promise 狀態機的 fulfilled 狀態的狀態變換器;而 reject 函數一樣是向外暴露,且用於修改 Promiserejected 狀態的狀態變換器。

另外,咱們拋開技術實現細節,從函數的抽象功能來看。不管是 fulfill 函數仍是 reject 函數它們起到的做用不外乎:

  1. 變換當前 Promise 實例狀態機的狀態至一個指定值。

  2. 被調用時,將接收外部傳遞的異步流操做的結果返回值,並將其在當前 Promise 實例中保存。

  3. 在實現了 Promise 狀態機狀態變換後,應該發送 異步通知 給全部的以前已經註冊的觀察當前狀態的回調函數。

基於以上三點和 Don't repeat yourself 原則,那麼咱們能夠從功能抽象的角度來實現一個 _settle 內部函數,用於實現 真正的狀態變換 功能。

interface ThenableCallbacks {
  onFulfilled?: (result?: any) => any
  onRejected?: (reason?: )
}

class Promise {
  private _observers: ThenableCallbacks[] = []
  // ...
  private _settle(state: Exclude<States, States.pending>, result: any) {
    if (state !== States.pending) return
    this.state = state
    this.value = result
    this._notify(state === States.fulfilled ? 'onFulfilled' : 'onRejected')
  }

  private _notify(type, message?: any) {}
  // ...
}
複製代碼

那麼在通知狀態的時,爲何要實現 異步通知 而不是直接經過 同步通知 觀察狀態變化的回調函數?

這是由於在 Promise/A+ 中已經明肯定義,全部 onFulfilledonRejected 回調函數,不能在當前執行上下文棧中執行,而是必須等到 一個全新的僅有平臺代碼的執行上下文棧 中執行。

平臺代碼是指僅有 JS 引擎,環境和 Promise 實現代碼而不含其餘業務代碼的場景。

如何實現回調函數調用

根據 ECMA 262 規範 8.3 Execution Contexts 章節模型,全部平臺 JS 代碼執行都是依賴於 執行上下文 execution context,其容器爲 執行上下文棧 execution context stack,而 執行上下文棧 是由 task queuemicro-task queue 來驅動。

在代碼執行完成時,即當前 執行上下文 移交 running execution context 的標誌時,會退出當前 執行上下文棧。在當前 running task/micro-task 執行完成之際,也就是當前 執行上下文棧 中清空後,會由下一個 task queue 中的 taskmicro-task queue 中的 micro-task 來建立一個 執行上下文棧 棧底的執行上下文,並繼續執行新的 task/micro-task 中的代碼。

在 JS 中執行上下文的類型分爲:函數執行上下文,全局執行上下文,eval 執行上下文(如無必須,不推薦使用,故不討論)。全部的函數執行都會建立一個新的執行上下文用於追蹤代碼執行,反之,其餘代碼執行都會在全局執行上下文中執行。

那麼依據上文,回調函數的執行必須等到當前執行上下文棧清空,並在一個空白的執行上下文棧來執行 onFulfilledonRejected 回調,即除開平臺代碼外,onFulfilledonRejected 回調函數必須是當前執行上下文棧中最靠近棧底的第一個執行上下文。

根據 JavaScript事件驅動模型

  1. 若須要將回調函數加入到 task queuequeue task,那麼咱們須要基於一個 task 引導 來執行待執行的回調函數,如 setTimeoutsetImmediate(限 IENode.js 環境);
  1. 若須要將回調函數加入到 micro-task queuequeue microtask,那麼咱們須要基於一個 micro-task 引導 將待執行回調函數加入到 micro-task queue 中,如 MutationObserver(限瀏覽器環境)、process.nextTick(限 Node.js 環境)。

本文也將依據以上理論,基於 setTimeout 來實現名爲 marcoTaskRunnertask 建立函數。

function marcoTaskRunner(this: any, fn: Function, ...args: any[]) {
  return setTimeout(() => fn.apply(this, args), 0)
}
複製代碼

那麼完善在新的執行上下文中執行 onFulfilledonRejected 回調函數的基礎後,不難寫出向全部回調函數觀察者廣播的 _notify 函數,並基於此獲得整個狀態修改後的通知觀察者流程:

settle promise

class Promise {
  private _observers: ThenableCallbacks[] = []

  // ...
  private _settle(state: Exclude<States, States.pending>, result: any) {
    if (state !== States.pending) return
    this.state = state
    this.value = result
    this._notify(state === States.fulfilled ? 'onFulfilled' : 'onRejected')
  }

  private _notify(type, message?: any) {
    this._observers.forEach(callbacks => {
      const handler = callbacks[type]
      if (isFunction(handler)) {
        // spec 2.2.5
        // https://promisesaplus.com/#point-35
        handler.call(null, message)
      }
    })
  }
  // ...
}
複製代碼

當在前文中 executor 的外部狀態轉換器 fulfillreject 函數觸發 Promise 內部狀態變換器後,將觸發狀態修改,結果賦值,調用回調函數一系列流程。這便是實現有序的異步流控制流程 Promise 方案的 技術核心 之一。

剩餘一些在初始化 Promise 階段的特定流程可在 the promise resolution procedure 找到。本文將省略已經在規範中明肯定義的普通初始化流程。

then 方法實現

Promise/A+ 中,定義了全部異步流控制的回調函數都是經過 then 函數來註冊。then 函數始終返回一個 新的 Promise 實例。

2.2.7 then must return a promise.

深刻思考一下,爲何 then 要返回一個新的 Promise 實例。這樣作的緣由是,then 函數返回的 Promise 實例可繼續被下一個 then 函數訪問,這樣循環往復,最終可實現 Promise 鏈式(級聯) 調用。

asyncAction()
  .then(function callbackA() {}, function catchA() {})
  .then(function callbackB() {}, function catchB() {})
  .then(function callbackC() {}, function catchC() {})
  .then(function callbackD() {}, function catchD() {})
複製代碼

爲何要支持 級聯調用

級聯調用 私覺得最大的 優點 在於開發者能夠將不一樣的異步流將一個繁瑣的異步流操做 分解 decouple 爲更小的異步流操做,而更小的操做意味着更多的操做組合可能性。這樣的優點可大大加強代碼靈活性,可讀性,維護性。全部的異步流操做再也不受限於傳入的 API 格式,若是有必要任意組合多個異步流操做。

在不一樣時期處理回調函數

class Promise<T> {
  // ...
  then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): Promise<T> {
    return new Promise<T>((resolve, reject) => { if (isStrictEql(this.state, States.pending)) { // TODO return } if (isStrictEql(this.state, States.fulfilled)) { // TODO return } if (isStrictEql(this.state, States.rejected)) { // TODO return } }) } } 複製代碼

Promise 顯式原型上的 then 方法將根據 Promise 狀態機可能的三種狀態進行三種不一樣類型的回調函數操做。

結合 Promises/A+2.2 The then method 中的定義:

  1. 不管是 onFulfilled 仍是 onRejected 回調函數,必須等待 this 所指向的 Promise 實例的狀態固定爲 fulfilledrejected 以後才能被調用。

    換句話說,當 this 所指的 Promise 實例爲 pending 狀態時,應該將 onFulfilledonRejected 設置爲當前 Promise 狀態的觀察者,經過 observer 模式實現狀態修改廣播。

    固然這裏也更加細緻地分別訂閱 fulfilled 狀態和 onRejected 狀態,進而基於 publish/subscribe 模式實現發佈狀態的 topic,進而觸發調用已經註冊的回調函數隊列。

  1. this 所引用的實例狀態爲 fulfilled 時,應該調用全部以前註冊的 onFulfilled 函數,並將 this 引用的實例的 value 字段做爲 onFulfilled 的參數傳入,以用來表示 Promise 實例所表明的異步流的操做結果。
  1. this 所引用的實例爲 rejected 狀態時,應該調用 onRejected 函數,並將 this 引用的實例的 value 值,做爲 onRejected 的參數傳入,以用來表示 Promise 實例被 rejected 的緣由。

那麼在基於以上對規範的抽象總結後,不可貴到:

class Promise<T> {
  // ...
  private _register(onFulfilled: OnFulfilled<T>, onRejected: OnRejected) {
    // 方案一:observer 模式,以回調集合爲基準,做爲 Promise 實例變化的觀察者
    this._observers.push({
      onFulfilled,
      onRejected
    })

    // 方案二:publish/subscribe 模式,以單個回調爲基準,訂閱特定的 topic
    // this._onFulfilledSubs.push(onFulfilled)
    // this._onRejectedSubs.push(onRejected)
  }

  // ...
  then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): Promise<T> {
    return new Promise<T>((resolve, reject) => { if (isStrictEql(this.state, States.pending)) { this._register( result => onFulfilled(result), reason => onRejected(reason) ) return } if (isStrictEql(this.state, States.fulfilled)) { return marcoTaskRunner(() => onFulfilled(this.value)) } if (isStrictEql(this.state, States.rejected)) { return marcoTaskRunner(() => onRejected(this.value)) } }) } } 複製代碼

那麼咱們是否是就最終實現了 Promise 顯式原型上的 then 方法了呢?

並無,由於在規範中,還定義了 then 方法的兩個參數都是 可選參數,而以上咱們對 then 方法的實現都是基於 onFulFilledonRejected 都是強制參數,且都爲函數的狀況。

class Promise<T> {
  // ...
  then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): Promise<T> {
    const createFulfilledHandler = (
      resolve: OnFulfilled<T>,
      reject: OnRejected
    ) => (result?: any) => {
      try {
        if (isFunction(onFulFilled)) {
          return resolve(onFulFilled(result))
        }
        return resolve(result)
      } catch (evaluationError) {
        reject(evaluationError)
      }
    }

    const createRejectedHandler = (
      resolve: OnFulfilled<T>,
      reject: OnRejected
    ) => (reason?: Error) => {
      try {
        if (isFunction(onRejected)) {
          return resolve(onRejected(reason))
        }
        return reject(reason)
      } catch (evaluationError) {
        reject(evaluationError)
      }
    }

    return new Promise<T>((resolve, reject) => { const handleFulfilled = createFulfilledHandler(resolve, reject) const handleRejected = createRejectedHandler(resolve, reject) if (isStrictEql(this.state, States.pending)) { this._register( result => handleFulfilled(result), reason => handleRejected(reason) ) return } if (isStrictEql(this.state, States.fulfilled)) { return marcoTaskRunner(() => handleFulfilled(this.value)) } if (isStrictEql(this.state, States.rejected)) { return marcoTaskRunner(() => handleRejected(this.value)) } }) } } 複製代碼

基於規範 then 函數的功能定義,將 fulfilledrejected 狀態分別對應的功能邏輯抽象出兩個高階函數 createFulfilledHandlercreateRejectedHandler。由兩個高階函數分別生成對應狀態的處理函數。

至此,以上便是 then 函數中的核心功能點:

  1. 依託前文已經實現的 Promise 實例化流程,then 函數不論在何種狀況下都始終返回一個新的 Promise 實例。

  2. then 函數內部會對 this 所引用的 Promise 實例的狀態進行鍼對性的回調函數處理:

    1. thispending 狀態的 Promise 實例,那麼在當前 event loop 中不會調用任何回調函數,而且,將全部回調函數與 Promise 造成綁定關係,結合前文中的狀態變換器,在發生任意的狀態固定時,將觸發對應的回調函數調用。

    2. thisfulfilled/rejected 狀態的 Promise 實例,即代表該 Promise 實例狀態已經固定,那麼指定狀態的回調函數將在下一輪 event loop 中被調用。

結論

以上筆者避免繁瑣地把 Promises/A+ 規範的一條條定義逐步進行闡述,而是着重在於闡述分析規範中的 核心思惟模式技術細節關鍵點。筆者私覺得技術實現方案千差萬別,實現的技術細節並非最有價值的東西,最有價值實際上是技術背後的思想。對於異步流控制解決方案來講,對於 Promises/A+ 來講,最核心的思惟模式是:

  1. 經過一個有限狀態機表示一個異步流控制流程。

  2. 經過狀態機內部狀態變化,觸發狀態修改,進而觸發回調隊列的順序調用。本質上是一種基於 observerpublish/subscribe 模式,回調函數與異步流狀態之間的相互依賴的關係。

  3. 規定 then 函數如 Promise 構造函數同樣始終返回一個新的 Promise 實例,使得 鏈式(級聯) 調用成爲了可能。

橫向對比

在深刻 Promises/A+ 規範以後,如雨後春筍般出現了各類具備不一樣適用場景的規範實現,應用最普遍的便是衆所周知的 ECMA 262 規範中的 JS 內置對象 —— Promise。那麼 Promise 內置對象以及一樣具備異步流控制能力的 async function 又是如何爲開發者提供高效簡潔的異步流控制?

ECMA 262 實現

根據最新的 ECMA 262 中對 Promise 章節的闡述,Promises/A+ 規範中 fulfilled 狀態在 ECMA 262 中被定義爲 resolved 狀態。Promise 內置對象不只僅實現了 Promises/A+ 標準,還在其基礎上進行了拓展,Promise 顯式原型對象上不只僅包含 then 函數,還包含了:

功能函數 使用場景
catch 捕獲 Promise 鏈中以前未被處理的 rejected 狀態。等價於 promise.then(null, function catch() {})
finally 只要 Promise 鏈以前的 Promise 狀態獲得固定(即不爲 pending 狀態),都始終觸發傳入 finally 的回調函數。
all 適用於多個異步流併發的場景,當存在一個包含多個 Promise 實例的列表時,只有當全部的 Promise 實例狀態都爲 resolved 狀態時,all 函數返回的 Promise 實例的狀態纔會爲 resolved
race 適用於多個異步流併發的場景,當存在一個包含多個 Promise 實例的列表時,第一個固定狀態的 Promise 狀態和值,會被賦值給當前 Promise.all 生成的 Promise 實例。
allSettled 適用於多個異步流併發的場景,當存在一個包含多個 Promise 實例的列表時,只有當全部 Promise 實例都固定狀態時纔會返回一個結果列表,該列表展現了對應索引 Promise 的狀態和結果值(或 rejected 緣由)。

本質上 JS 中的 Promise 內置對象是 Promises/A+ 的實現的拓展版本,在完成了規範定義的要素和功能以後,又在其核心功能上拓展出了以上幾個便捷的 API 用於處理異步流控制。

async function 又是什麼

async function 在實際應用中表現爲 async ... await 表達式,它的出現一樣是爲了給予開發者異步流控制的能力。

async function 的出現是爲了什麼?如前文闡述,Promise 支持級聯調用,那麼可能會出現一種狀況——過長的級聯調用鏈。而 async function 給開發者提供了一種 以同步的方式 來書寫異步流程的可能。

  • 級聯調用鏈
asyncAction()
  .then(function callbackA() {}, function catchA() {})
  .then(function callbackB() {}, function catchB() {})
  .then(function callbackC() {}, function catchC() {})
  .then(function callbackD() {}, function catchD() {})
複製代碼
  • 以同步的方式書寫異步流程
async function newAsyncActionChain() {
  try {
    const resultA = await callbackA()
  } catch (error) {
    catchA(error)
  }

  try {
    const resultB = await callbackB()
  } catch (error) {
    catchB(error)
  }
  // ...
}
複製代碼

這樣的同步方式更加符合人類的思惟定勢。可是 async function 一樣具備缺陷,每一次經過 async await 表達式獲取異步操做結果都須要使用 try...catch 語句來捕獲全部異步流的 rejected 狀態。

對比 async functionPromise 這兩種異步流控制解決方案,它們各有優點,又各有弊端:

異步流控制 相對優點 相對劣勢
async function 以同步思惟寫異步 須要使用額外代碼 try...catch 處理異常
Promise 級聯調用,可拆分更加細緻的異步流控制 可能出現過長的級聯調用鏈

另外,若是讀者在使用 TypeScript 且設置較低版本的編譯目標的後, TypeScript 會將代碼中的 async function 編譯爲基於 Promise 函數和 Generator 函數的結合體。 而其餘一些流行的 async function 方案,如 @babel/plugin-transform-async-to-generator 一樣會將 async function 轉換爲 Generator 函數。

不管是從哪一個實現上來看,async function 的流行 polyfill 都離不開 Generator 函數,由於 Generator 函數天生具備 暫停 函數執行的能力。這一點正好符合 await 語句的功能點。async function 本質上能夠認爲是一個具備自動執行 next() 功能的 Generator 函數。

結論

經過以上對比發現,不管是 Promise 內置對象所提供的級聯調用,仍是 async function 的以同步方式書寫異步流程,它們的出現都是爲了更好的解決開發時各類異步流控制的問題。

結合全文來看,不管是經過什麼樣的方式實現異步流控制,其本質上,都是回調函數與異步流狀態必須造成一種對應關係,在這種關係下,全部的回調函數都依賴於異步流狀態的值的改變。在異步流狀態改變的狀況下,若存在對應的回調函數,就會被調用。

相關文章
相關標籤/搜索