我的github部落格 github.com/zhuanyongxi…
大家都知道Promise解決了回撥地獄的問題。說到回撥地獄,很容易想到下面這個容易讓人產生誤解的圖片:
可回撥地獄到底是什麼?它到底哪裡有問題?是因為巢狀不好看還是讀起來不方便?
首先我們要想想,巢狀到底哪裡有問題?
舉個例子:
function a() {
function b() {
function c() {
function d() {}
d();
}
c();
}
b();
}
a();
複製程式碼
這也是巢狀,雖然好像不是特別美觀,可我們並不會覺得這有什麼問題吧?因為我們經常會寫出類似的程式碼。
在這個例子中的巢狀的問題僅僅是縮排的問題,而縮排除了會讓程式碼變寬可能會造成讀程式碼的一點不方便之外,並沒有什麼其他的問題。如果僅僅是這樣,為什麼不叫“縮排地獄”或“巢狀地獄”?
把回撥地獄完全理解成縮排的問題是常見的對回撥地獄的誤解。要回到“回撥地獄”這個詞語上面來,它的重點就在於“回撥”,而“回撥”在JS中應用最多的場景當然就是非同步程式設計了。
所以,“回撥地獄”所說的巢狀其實是指非同步的巢狀。它帶來了兩個問題:可讀性的問題和信任問題。
可讀性的問題
這是一個在網上隨便搜尋的關於執行順序的面試題:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(new Date, i);
}, 1000);
}
console.log(new Date, i);
複製程式碼
答案是什麼大家自己想吧,這不是重點。重點是,你要想一會兒吧?
一個整潔的回撥:
listen( "click", function handler( evt){
setTimeout( function request(){
ajax( "http:// some. url. 1", function response( text){
if (text == "hello") {
handler();
} else if (text == "world") {
request();
}
});
}, 500);
});
複製程式碼
如果非同步的巢狀都是這樣乾淨整潔,那“回撥地獄”給程式猿帶來的傷害馬上就會減少很多。
可我們實際在寫業務邏輯的時候,真實的情況應該是這樣的:
listen( "click", function handler(evt){
doSomething1();
doSomething2();
doSomething3();
doSomething4();
setTimeout( function request(){
doSomething8();
doSomething9();
doSomething10();
ajax( "http:// some. url. 1", function response( text){
if (text == "hello") {
handler();
} else if (text == "world") {
request();
}
});
doSomething11();
doSomething12();
doSomething13();
}, 500);
doSomething5();
doSomething6();
doSomething7();
});
複製程式碼
這些“doSomething”有些是非同步的,有些是同步。這樣的程式碼讀起來會非常的吃力,因為你要不停的思考他們的執行順序,並且還要記在腦袋裡面。這就是非同步的巢狀帶來的可讀性的問題,它是由非同步的執行機制引起的。
信任問題
這裡主要用非同步請求討論。我們在做AJAX請求的時候,一般都會使用一些第三方的工具庫(即便是自己封裝的,也可以在一定程度上理解成第三方的),這就會帶來一個問題:這些工具庫是否百分百的可靠?
一個來自《YDKJS》的例子:一個程式設計師開發了一個付款的系統,它良好的執行了很長時間。突然有一天,一個客戶在付款的時候信用卡被連續刷了五次。這名程式設計師在調查了以後發現,一個第三方的工具庫因為某些原因把付款回撥執行了五次。在與第三方團隊溝通之後問題得到了解決。
故事講完了,可問題真的解決了嗎?是否還能夠充分的信任這個工具庫?信任依然要有,可完善必要的檢查和錯誤處理勢在必行。當我們解決了這個問題,由於它的啟發,我們還會聯想到其他的問題,比如沒有呼叫回撥。
再繼續想,你會發現,這樣的問題還要好多好多。總結一下可能會出現的問題:
- 回撥過早(一般是非同步被同步呼叫);
- 回撥過晚或沒有回撥;
- 回撥次數過多;
- 等等
加上了這些檢查,強壯之後的程式碼可能是這樣的:
listen( "click", function handler( evt){
check1();
doSomething1();
setTimeout( function request(){
check2();
doSomething3();
ajax( "http:// some. url. 1", function response( text){
if (text == "hello") {
handler();
} else if (text == "world") {
request();
}
});
doSomething4();
}, 500);
doSomething2();
});
複製程式碼
我們都清楚的知道,實際的check
要比這裡看起來的複雜的多,而且很多很難複用。這不但使程式碼變得臃腫不堪,還進一步加劇了可讀性的問題。
雖然這些錯誤出現的概率不大,但我們依然必須要處理。
這就是非同步巢狀帶來的信任問題,它的問題的根源在於控制反轉。控制反轉在物件導向中的應用是依賴注入,實現了模組間的解耦。而在回撥中,它就顯得沒有那麼善良了,控制權被交給了第三方,由第三方決定什麼時候呼叫回撥以及如何呼叫回撥。
一些解決信任問題的嘗試
加一個處理錯誤的回撥
function success(data) {
console. log(data);
}
function failure(err) {
console. error( err );
}
ajax( "http:// some. url. 1", success, failure );
複製程式碼
nodejs的error-first
function response(err, data) {
if (err) {
console. error( err );
}
else {
console. log( data );
}
}
ajax( "http:// some. url. 1", response );
複製程式碼
這兩種方式解決了一些問題,減少了一些工作量, 但是依然沒有徹底解決問題。首先它們的可複用性依然不強,其次,如回撥被多次呼叫的問題依然無法解決。
Promise如何解決這兩個問題
Promise已經是原生支援的API了,它已經被加到了JS的規範裡面,在各大瀏覽器中的執行機制是相同的。這樣就保證了它的可靠。
如何解決可讀性的問題
這一點不用多說,用過Promise的人很容易明白。Promise的應用相當於給了你一張可以把解題思路清晰記錄下來的草稿紙,你不在需要用腦子去記憶執行順序。
如何解決信任問題
Promise並沒有取消控制反轉,而是把反轉出去的控制再反轉一次,也就是反轉了控制反轉。
這種機制有點像事件的觸發。它與普通的回撥的方式的區別在於,普通的方式,回撥成功之後的操作直接寫在了回撥函式裡面,而這些操作的呼叫由第三方控制。在Promise的方式中,回撥只負責成功之後的通知,而回撥成功之後的操作放在了then的回撥裡面,由Promise精確控制。
Promise有這些特徵:只能決議一次,決議值只能有一個,決議之後無法改變。任何then中的回撥也只會被呼叫一次。Promise的特徵保證了Promise可以解決信任問題。
對於回撥過早的問題,由於Promise只能是非同步的,所以不會出現非同步的同步呼叫。即便是在決議之前的錯誤,也是非同步的,並不是會產生同步(呼叫過早)的困擾。
var a = new Promise((resolve, reject) => {
var b = 1 + c; // ReferenceError: c is not defined,錯誤會在下面的a列印出來之後報出。
resolve(true);
})
console.log(1, a);
a.then(res => {
console.log(2, res);
})
.catch(err => {
console.log(err);
})
複製程式碼
對於回撥過晚或沒有呼叫的問題,Promise本身不會回撥過晚,只要決議了,它就會按照規定執行。至於伺服器或者網路的問題,並不是Promise能解決的,一般這種情況會使用Promise的競態APIPromise.race
加一個超時的時間:
function timeoutPromise(delay) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject("Timeout!");
}, delay);
});
}
Promise.race([doSomething(), timeoutPromise(3000)])
.then(...)
.catch(...);
複製程式碼
對於回撥次數太少或太多的問題,由於Promise只能被決議一次,且決議之後無法改變,所以,即便是多次回撥,也不會影響結果,決議之後的呼叫都會被忽略。
參考資料: