問題
在Ajax應用中,調用XMLHttpRequest是很常見的情況。特別是以用戶端為中心的Ajax應用,各種需要從伺服器端擷取資料的操作都通過XHR非同步呼叫完成。然而在單線程的JavaScript編程中,XHR非同步呼叫的代碼風格實在是與一般的JavaScript代碼格格不入。
額外參數
考慮一個除法函數,如果它是純用戶端的同步函數,那麼簽名會是這樣的:
function divide(operand1, operand2)
然而假設我們對用戶端除法的精度不滿意,於是把除法轉移到伺服器端來執行,那麼它是個需要調用XHR的非同步函數,簽名也就可能會是以下幾種之一:
function divide(operand1, operand2, callback)
function divide(operand1, operand2, successCallback, failureCallback)
function divide(operand1, operand2, options)
我們必須在簽名中引入新的參數來傳遞迴調函數,不能選擇讓函數變成阻塞式的同步調用。
可傳遞性
不僅僅直接操作XHR的函數需要引入新的參數,這種複雜性還會順著調用棧向外傳遞。例如說,我們對加減乘除四則運算作了封裝,只向外暴露一個運算介面:
function calculate(operand1, operand2, operator)
這個calculate函數根據operator參數來調用內部的plus, subtract, multiply, divide函數。然而,因為divide函數變成了非同步函數,所以整個calculate函數不得不也轉變為非同步函數:
function calculate(operand1, operand2, operator, callback)
同時,在調用棧之上凡是需要調用到calculate的函數,都必須變成非同步,除非它並不需要向上一級調用函數返回結果。
同步並存
儘管calculate函數變成了一個非同步函數,它所調用的plus, subtract, multiply函數還是同步函數,只有調用divide時是非同步,這時候calculate就是一個非同步同步並存函數。
這會帶來什麼問題?calculate的調用者看到函數簽名自然會認為calculate是個非同步函數,因為它需要傳遞迴調函數,然而calculate的執行方式卻是不確定的。考慮如下調用:
calculate(operand1, operand2, operator, callback);
next();
這裡涉及到callback和next兩個函數,它們哪個先執行哪個後執行是不確定的,或者說是依賴於calculate具體實現的。
如果calculate的實現是,當不需要進行非同步作業時,直接調用callback。那麼,在執行加減乘法時callback會在next之前被調用;在執行除法時next會在callback之前調用。
如果我們不喜歡這種不確定性,我們可以改變一下calculate的實現,把同步調用也都改為setTimeout形式的,這樣在任何情況下next都一定會在callback之前被調用。
然而後面一種做法依賴於成本較高的實現方式,開發人員一個不小心(或者擺明偷懶)就會漏掉setTimeout,導致函數調用順序變得不確定,所以我們會希望這是架構協助實現的功能,在使用架構時無法把這繞過。
情境
在這裡,我舉一個關於上述問題的具體應用情境。(為簡化問題,描述已略作修改,與實際應用並不一致。)
在百度Hi網頁版裡面,我們會在用戶端儲存一個使用者物件列表,在開啟和這個使用者的聊天視窗時,我們需要從中讀取這個使用者的資訊。這個操作就涉及很多可能同步又可能非同步分支:
使用者物件未緩衝
非同步讀取使用者資訊
使用者物件已緩衝
使用者是好友(資訊更新會由伺服器端推送)
同步讀取使用者資訊
使用者不是好友(資訊更新需要由用戶端拉取)
可以接受緩衝資訊
同步讀取使用者資訊
必須擷取最新資訊
非同步讀取使用者資訊
可以看到,分支的結果最終既有同步的,也有非同步。並且這些分支還不是在一個函數裡完成,而是在幾個函數裡實現。也就是說,按照傳統的模式,這些函數一部分是同步的,一部分是非同步,由於非同步傳遞性,最終調用棧頂層的函數都是非同步。
為瞭解決這個問題,我們需要寫一個非同步呼叫架構,用一種統一的方式來進行調用,把同步和非同步呼叫都合并為一種返回方式。