同步發表於 個人博客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+
一個略微中二的名字,但它給社區給開發者帶來了異步操做流的重磅炸彈—— Promise
。 規範定義了一個名爲 Promise
的對象來承載當下異步操做流的 最終結果。它用同步的方式表示了當下異步操做流的狀態以及與狀態相關聯的返回值。
在操做成功的狀況下,異步操做的返回值 value
是多少;
在操做失敗的狀況下,又是由於什麼緣由 reason
失敗的。
// 僅藉助 ts 的接口來表示 Promise 的 **抽象** 結構,不表明實現細節
interface Promise {
state: PromiseState
value: any // 不管是成功仍是失敗,都經過該字段存儲對應的值或失敗緣由
}
複製代碼
與 Promise
對象交互的 首要方式 都是經過一個名爲 then
的方法來實現,如修改 Promise
的狀態,返回異步操做返回值或異步操做失敗緣由。
那麼在定義了這樣的規範核心的前提下,全部的規範內容都是圍繞實現以上兩點核心功能點。
根據上文中 Promises/A+
規範對 Promise
對象的定義。從本質上來說,Promise
是一種抽象的狀態模型,一種 有限狀態機。規範在第一章首要位置首先定義了 3 個值來表示 Promise
對象的狀態,即表示了全部異步操做流可能出現的三種狀態:
pending
狀態,如字面意思同樣,表示當下異步操做流正在執行中,正在 等待 異步流操做的結果。
fulfill
狀態,表示當下異步操做流已經操做 成功,並在 Promise
對象中包含了當下操做的執行結果。
本文遵循 Promise/A+
的表示方式,fulfill
等價於 ES6 Promise
中的 resolve
狀態。
interface FulfilledPromise {
state: States.fulfilled
value: any
}
複製代碼
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+
規範,首先拋開實現技術細節上來看,全部的異步操做流程都具備:
註冊回調
由於是異步流控制,異步流內部相對於外部模塊來講始終是在進行異步操做,那麼在執行異步操做開始,進行中,結束時的任意階段都首先經過異步操做模塊對外暴露接口 註冊一個或多個回調函數。
回調地獄只能在異步流開始以前進行回調註冊,而
Promises/A+
定義了同一個Promise
實例能夠調用屢次then
函數,即實現了多階段多個回調註冊。
觸發回調
在異步流再也不爲 pending
狀態時,那麼給對應狀態註冊的全部回調函數,會 依據註冊順序 分別獲得執行。
經過對關鍵點的梳理,全部的回調函數都是直接依賴於異步流的狀態的,那麼它們都是當前異步流狀態變化的 觀察者,而當前異步流的狀態變化始終是全部回調函數的 主題。結合兩者的關係分析,不難經過 observer 模式實現異步流控制的 技術核心。
這裏筆者的實現是將 then
回調傳入的回調函數 onFulFilled
和 onRejected
合二爲一看待,它們造成一個 回調函數
總體 ThenableCallbacks。該集合總體 直接依賴 於當前的 Promise
實例,當前實例做爲全部回調集合主題 subject
。側重於集合總體觀察 Promise
狀態變換,而非具體狀態值。在存在 Promise
狀態值變換,即 subject
變化時,將廣播至每個回調集合。故說是經過 observer
模式(而不是 publish/subscribe
模式)。
interface ThenableCallbacks {
onFulfilled?: OnfulfilledCallback
onRejected?: OnRejectedCallback
}
複製代碼
ThenableCallbacks --觀察--> Promise.state 的變化
複製代碼
固然存在另一種抽象思路是,將 onFulfilled
和 onRejected
都認爲是獨立的個體,它們分別經過 fulfilledQueue
和 rejectedQueue
來 間接依賴 當前的 Promise
實例。而且 fulfilledQueue
和 rejectedQueue
經過 message broker
或 event bus
來訂閱各自的指望主題 topic
。這個時候整個模式側重的再也不是 Promise
實例的狀態變化,而是實例的某一個具體的狀態值。全部的回調函數的觸發都是經過 message broker
或 event 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)
。
這裏 promise
就是在初始化 Promise
構造函數所返回的 Promise
實例,記爲 $0
。它始終是對外暴露的。
單值 x
是做爲當前異步流的結果載體,它自己是表示一個異步流的結果返回值,不該被模塊外部直接修改。當 x
表示一個非 Promise
實例的值時,它的存在會直接被當前 $0
實例直接引用爲 Promise
的實例 value
字段的值。
細心的讀者可能發現,x
值是能夠爲另外一個 Promise
實例的,那麼在這種狀況下,當前 $0
實例會嘗試直接同步 x
變量所引用的 Promise
實例的狀態 state
,並將該實例的 value
字段的異步流結果值,直接賦值給 $0
實例的 value
字段。
在基於以上兩點的前提下,咱們在初始化 Promise
階段的核心目標是,實現一個有限狀態機,並接收一個 executor
來 接收 狀態機對外的暴露的狀態變換器。該變換器的核心目標是,在被調用的狀況下,修改狀態機內部的狀態值。另外,基於 Promises/A+
規範中的定義,全部已經固定的狀態,沒法被再次修改。
在 Promise
實例的狀態被狀態變換器觸發改變的條件下,同時經過狀態變換器來接收執行結果或執行錯誤。並將執行結果或執行錯誤賦值給 Promise
的 value
字段,以供 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
函數一樣是向外暴露,且用於修改 Promise
的 rejected
狀態的狀態變換器。
另外,咱們拋開技術實現細節,從函數的抽象功能來看。不管是 fulfill
函數仍是 reject
函數它們起到的做用不外乎:
變換當前 Promise
實例狀態機的狀態至一個指定值。
被調用時,將接收外部傳遞的異步流操做的結果返回值,並將其在當前 Promise
實例中保存。
在實現了 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+
中已經明肯定義,全部 onFulfilled
或 onRejected
回調函數,不能在當前執行上下文棧中執行,而是必須等到 一個全新的僅有平臺代碼的執行上下文棧 中執行。
平臺代碼是指僅有 JS 引擎,環境和 Promise 實現代碼而不含其餘業務代碼的場景。
根據 ECMA 262
規範 8.3 Execution Contexts 章節模型,全部平臺 JS
代碼執行都是依賴於 執行上下文 execution context,其容器爲 執行上下文棧 execution context stack,而 執行上下文棧
是由 task queue 和 micro-task queue 來驅動。
在代碼執行完成時,即當前 執行上下文
移交 running execution context
的標誌時,會退出當前 執行上下文棧
。在當前 running task/micro-task
執行完成之際,也就是當前 執行上下文棧
中清空後,會由下一個 task queue
中的 task 或 micro-task queue
中的 micro-task
來建立一個 執行上下文棧
棧底的執行上下文,並繼續執行新的 task/micro-task
中的代碼。
在 JS 中執行上下文的類型分爲:函數執行上下文,全局執行上下文,eval 執行上下文(如無必須,不推薦使用,故不討論)。全部的函數執行都會建立一個新的執行上下文用於追蹤代碼執行,反之,其餘代碼執行都會在全局執行上下文中執行。
那麼依據上文,回調函數的執行必須等到當前執行上下文棧清空,並在一個空白的執行上下文棧來執行 onFulfilled
或 onRejected
回調,即除開平臺代碼外,onFulfilled
或 onRejected
回調函數必須是當前執行上下文棧中最靠近棧底的第一個執行上下文。
根據 JavaScript
的事件驅動模型:
task queue
中queue task,那麼咱們須要基於一個 task 引導
來執行待執行的回調函數,如 setTimeout
或 setImmediate
(限 IE
或 Node.js
環境);micro-task queue
中queue microtask,那麼咱們須要基於一個 micro-task 引導
將待執行回調函數加入到 micro-task queue
中,如 MutationObserver
(限瀏覽器環境)、process.nextTick
(限 Node.js
環境)。本文也將依據以上理論,基於 setTimeout
來實現名爲 marcoTaskRunner
的 task
建立函數。
function marcoTaskRunner(this: any, fn: Function, ...args: any[]) {
return setTimeout(() => fn.apply(this, args), 0)
}
複製代碼
那麼完善在新的執行上下文中執行 onFulfilled
或 onRejected
回調函數的基礎後,不難寫出向全部回調函數觀察者廣播的 _notify
函數,並基於此獲得整個狀態修改後的通知觀察者流程:
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
的外部狀態轉換器 fulfill
或 reject
函數觸發 Promise
內部狀態變換器後,將觸發狀態修改,結果賦值,調用回調函數一系列流程。這便是實現有序的異步流控制流程 Promise
方案的 技術核心 之一。
剩餘一些在初始化 Promise
階段的特定流程可在 the promise resolution procedure 找到。本文將省略已經在規範中明肯定義的普通初始化流程。
在 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 中的定義:
不管是 onFulfilled
仍是 onRejected
回調函數,必須等待 this
所指向的 Promise
實例的狀態固定爲 fulfilled
或 rejected
以後才能被調用。
換句話說,當 this
所指的 Promise
實例爲 pending
狀態時,應該將 onFulfilled
和 onRejected
設置爲當前 Promise
狀態的觀察者,經過 observer
模式實現狀態修改廣播。
固然這裏也更加細緻地分別訂閱 fulfilled
狀態和 onRejected
狀態,進而基於 publish/subscribe
模式實現發佈狀態的 topic
,進而觸發調用已經註冊的回調函數隊列。
this
所引用的實例狀態爲 fulfilled
時,應該調用全部以前註冊的 onFulfilled
函數,並將 this
引用的實例的 value
字段做爲 onFulfilled
的參數傳入,以用來表示 Promise
實例所表明的異步流的操做結果。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
方法的實現都是基於 onFulFilled
和 onRejected
都是強制參數,且都爲函數的狀況。
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
函數的功能定義,將 fulfilled
和 rejected
狀態分別對應的功能邏輯抽象出兩個高階函數 createFulfilledHandler
和 createRejectedHandler
。由兩個高階函數分別生成對應狀態的處理函數。
至此,以上便是 then
函數中的核心功能點:
依託前文已經實現的 Promise
實例化流程,then
函數不論在何種狀況下都始終返回一個新的 Promise
實例。
then
函數內部會對 this
所引用的 Promise
實例的狀態進行鍼對性的回調函數處理:
若 this
爲 pending
狀態的 Promise
實例,那麼在當前 event loop
中不會調用任何回調函數,而且,將全部回調函數與 Promise
造成綁定關係,結合前文中的狀態變換器,在發生任意的狀態固定時,將觸發對應的回調函數調用。
若 this
爲 fulfilled/rejected
狀態的 Promise
實例,即代表該 Promise
實例狀態已經固定,那麼指定狀態的回調函數將在下一輪 event loop
中被調用。
以上筆者避免繁瑣地把 Promises/A+
規範的一條條定義逐步進行闡述,而是着重在於闡述分析規範中的 核心思惟模式 和 技術細節關鍵點。筆者私覺得技術實現方案千差萬別,實現的技術細節並非最有價值的東西,最有價值實際上是技術背後的思想。對於異步流控制解決方案來講,對於 Promises/A+
來講,最核心的思惟模式是:
經過一個有限狀態機表示一個異步流控制流程。
經過狀態機內部狀態變化,觸發狀態修改,進而觸發回調隊列的順序調用。本質上是一種基於 observer
或 publish/subscribe
模式,回調函數與異步流狀態之間的相互依賴的關係。
規定 then
函數如 Promise
構造函數同樣始終返回一個新的 Promise
實例,使得 鏈式(級聯)
調用成爲了可能。
在深刻 Promises/A+
規範以後,如雨後春筍般出現了各類具備不一樣適用場景的規範實現,應用最普遍的便是衆所周知的 ECMA 262 規範中的 JS
內置對象 —— Promise
。那麼 Promise
內置對象以及一樣具備異步流控制能力的 async function
又是如何爲開發者提供高效簡潔的異步流控制?
根據最新的 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 ... 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 function
和 Promise
這兩種異步流控制解決方案,它們各有優點,又各有弊端:
異步流控制 | 相對優點 | 相對劣勢 |
---|---|---|
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
的以同步方式書寫異步流程,它們的出現都是爲了更好的解決開發時各類異步流控制的問題。
結合全文來看,不管是經過什麼樣的方式實現異步流控制,其本質上,都是回調函數與異步流狀態必須造成一種對應關係,在這種關係下,全部的回調函數都依賴於異步流狀態的值的改變。在異步流狀態改變的狀況下,若存在對應的回調函數,就會被調用。