異步進(jìn)化史
異步在實(shí)現(xiàn)上,依賴一些特殊的語法規(guī)則。從整體上來說,異步方案經(jīng)歷了如下的四個進(jìn)化階段:
回調(diào)函數(shù) —> Promise —> Generator —> async/await
其中 Promise、Generator 和 async/await 都是在 ES2015 之后,慢慢發(fā)展起來的、具有一定顛覆性的新異步方案。相較于 “回調(diào)函數(shù) “時期的刀耕火種而言,具有劃時代的意義。
“回調(diào)函數(shù)”時期存在的問題
-
回調(diào)嵌套 -> 理解問題,缺乏順序性
場景:根據(jù)第一個網(wǎng)絡(luò)請求的結(jié)果,再去執(zhí)行第二個網(wǎng)絡(luò)請求;然后根據(jù)第二個網(wǎng)絡(luò)請求的結(jié)果執(zhí)行第三個網(wǎng)絡(luò)請求... 于是出現(xiàn)了如下的代碼,臭名昭著的“回調(diào)地獄”現(xiàn)身。
請求1(function(請求結(jié)果1){
請求2(function(請求結(jié)果2){
請求3(function(請求結(jié)果3){
請求4(function(請求結(jié)果4){
請求5(function(請求結(jié)果5){
請求6(function(請求結(jié)果3){
...
})
})
})
})
})
})
這種嵌套的書寫方式,排查問題時我們需要繞過很多障眼法,不斷的在函數(shù)間跳轉(zhuǎn),甚至需要花費(fèi)一些時間去思考真正的執(zhí)行順序。嵌套和縮進(jìn)只是回調(diào)地獄的一個梗,它導(dǎo)致的問題遠(yuǎn)不止嵌套導(dǎo)致的可讀性降低。
大腦對于事情的計(jì)劃方式時線性的、阻塞的、單線程的語義,但是回調(diào)表達(dá)異步流程的方式是非線性的、非順序的,這使得正確推導(dǎo)這樣的代碼難度很大。難以理解的代碼是壞代碼,會導(dǎo)致壞bug。 回調(diào)地獄帶來的負(fù)作用有以下幾點(diǎn):
- 代碼臃腫
- 可讀性差
- 耦合度過高,可維護(hù)性差
- 代碼復(fù)用性差
- 容易滋生bug
- 只能再回調(diào)里處理異常
我們需要一種更同步、更順序、更阻塞的方式來表達(dá)異步,就像我們的大腦一樣。
-
控制反轉(zhuǎn) -> 信任問題
A和B發(fā)生于現(xiàn)在,在JavaScript主程序的直接控制之下,而C會延遲到將來發(fā)生,并且是在第三方的控制下(多數(shù)情況下,是某個第三方提供的工具)。這種控制的轉(zhuǎn)移通常不會給程序帶來很多問題。
我們用回調(diào)函數(shù)來封裝程序中的continuation,然后把回調(diào)交給第三方(甚至可能是外部代碼),接著期待其能夠調(diào)用回調(diào),實(shí)現(xiàn)正確的功能。這種稱為 控制反轉(zhuǎn) ,就是把自己程序一部分的執(zhí)行控制交給某個第三方。第三方提供某個工具,你傳入回調(diào)處理自己的邏輯,由于你的代碼和第三方工具之間沒有一份明確表達(dá)的契約,他們調(diào)用你的回調(diào)時可能出現(xiàn)一些情況:
- 調(diào)用回調(diào)過早
- 調(diào)用回調(diào)過晚(或者沒有調(diào)用)
- 調(diào)用回調(diào)的次數(shù)太少或太多
- 沒有把所需的環(huán)境/參數(shù)成功傳給你的回調(diào)函數(shù)
- 吞掉可能出現(xiàn)的錯誤或異常
// A
ajax("..", function(){
// C
});
// B
對于被傳給你無法信任的工具的每個問題,你都將不得不創(chuàng)建大量的混亂邏輯,此時是否更加明白回調(diào)地獄是多像地獄了吧!
回調(diào)最大的問題是控制反轉(zhuǎn),它會導(dǎo)致信任鏈的完全斷裂!
回調(diào)的變體
回調(diào)設(shè)計(jì)存在幾個變體,意在解決前面討論的一些信任問題(不是全部?。?/p>
分離回調(diào)
為了更優(yōu)雅地處理錯誤,有些API設(shè)計(jì)提供了 分離回調(diào)(一個用于成功通知,一個用于出錯通知)
function success(data){
console.log(data);
}
function failure(err){
console.error(err);
}
ajax("http://some.url.1", success, failure);
這種設(shè)計(jì),API的出錯處理函數(shù) failure() 常常是可選的,如果沒有提供的話,就是假定這個錯誤可以吞掉。ES6 Promise API使用的就是這種分離回調(diào)設(shè)計(jì)。
error-first
error-first風(fēng)格 回調(diào)模式,也稱Node風(fēng)格,因?yàn)閹缀跛蠳ode.js API都采用這種風(fēng)格。
其中回調(diào)的第一個參數(shù)保留用作錯誤對象。如果成功的話,這個參數(shù)就會被清空/置假(后續(xù)的參數(shù)就是成功數(shù)據(jù))。如果產(chǎn)生了錯誤結(jié)果,第一個參數(shù)就會被置起/置真(通常就不會再傳遞其他結(jié)果)。
function response(err, data){
if(err){
console.error(err)
}else{
console.log(data)
}
}
ajax("http://some.url.1", response);
存在問題
這并沒有像表面看上去那樣真正解決主要的信任問題,并沒有涉及阻止或過濾不想要的重復(fù)調(diào)用回調(diào)的問題?,F(xiàn)在事情更糟糕,因?yàn)楝F(xiàn)在你可能同時得到成功或失敗的結(jié)果,或者都沒有,并且你還不得不編碼處理這些情況。
盡管這是可采用的標(biāo)準(zhǔn)模式,但更加冗長和模式化,可復(fù)用性不高,還得給應(yīng)用中的每個回調(diào)添加這樣的代碼。
??? 如何解決完全不調(diào)用的信任問題?設(shè)置一個超時來取消事件
??? 如何解決調(diào)用過早的信任問題?永遠(yuǎn)要異步,創(chuàng)建一個類似于驗(yàn)證概念版本的asyncify()工具
雖然可以寫一些特點(diǎn)邏輯來解決這些信任問題,但其難度高于應(yīng)有的水平,可能會產(chǎn)生更笨重、更難維護(hù)的代碼,并且缺少足夠的保護(hù),其中的損害要直到你受到bug的影響才會被發(fā)現(xiàn)。
我們需要一個通用的方案來解決這些信任問題。不管我們創(chuàng)建多少回調(diào),這一方案都應(yīng)可以復(fù)用,且沒有重復(fù)代碼的開銷。
異常處理
try…catch是同步代碼,只能捕獲“同步代碼”中的"運(yùn)行時異常","同步代碼"是無法獲取如setTimeout、Promise等異步代碼的異常。
Q:為什么 try...catch 無法直接捕獲異步的錯誤?
比如執(zhí)行 fs.readdir 的時候,其實(shí)是將回調(diào)函數(shù)加入任務(wù)隊(duì)列中,代碼繼續(xù)執(zhí)行,直至主線程完成后,才會從任務(wù)隊(duì)列中選擇已完成的任務(wù),并將其加入棧中,此時棧中只有這一個執(zhí)行上下文,如果回調(diào)報(bào)錯,也無法獲取調(diào)用該異步操作時的棧中的信息,不容易判定哪里出現(xiàn)了錯誤。
因此,要處理 setTimeout 等回調(diào)內(nèi)部的異常,只能將 try-catch 放置到回調(diào)內(nèi)部。