問題
在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,導致函數調用順序變得不確定,所以我們會希望這是框架幫助實現的功能,在使用框架時無法把這繞過。