異步進(jìn)化史
異步在實(shí)現(xiàn)上愚臀,依賴一些特殊的語法規(guī)則忆蚀。從整體上來說,異步方案經(jīng)歷了如下的四個(gè)進(jìn)化階段:
回調(diào)函數(shù) —> Promise —> Generator —> async/await
其中 Promise姑裂、Generator 和 async/await 都是在 ES2015 之后馋袜,慢慢發(fā)展起來的、具有一定顛覆性的新異步方案舶斧。相較于 “回調(diào)函數(shù) “時(shí)期的刀耕火種而言欣鳖,具有劃時(shí)代的意義。
“回調(diào)函數(shù)”時(shí)期存在的問題
-
回調(diào)嵌套 -> 理解問題茴厉,缺乏順序性
場景:根據(jù)第一個(gè)網(wǎng)絡(luò)請求的結(jié)果泽台,再去執(zhí)行第二個(gè)網(wǎng)絡(luò)請求;然后根據(jù)第二個(gè)網(wǎng)絡(luò)請求的結(jié)果執(zhí)行第三個(gè)網(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í)我們需要繞過很多障眼法嗜闻,不斷的在函數(shù)間跳轉(zhuǎn)蜕依,甚至需要花費(fèi)一些時(shí)間去思考真正的執(zhí)行順序。嵌套和縮進(jìn)只是回調(diào)地獄的一個(gè)梗琉雳,它導(dǎo)致的問題遠(yuǎn)不止嵌套導(dǎo)致的可讀性降低样眠。
大腦對于事情的計(jì)劃方式時(shí)線性的、阻塞的翠肘、單線程的語義檐束,但是回調(diào)表達(dá)異步流程的方式是非線性的、非順序的锯茄,這使得正確推導(dǎo)這樣的代碼難度很大厢塘。難以理解的代碼是壞代碼茶没,會(huì)導(dǎo)致壞bug。 回調(diào)地獄帶來的負(fù)作用有以下幾點(diǎn):
- 代碼臃腫
- 可讀性差
- 耦合度過高晚碾,可維護(hù)性差
- 代碼復(fù)用性差
- 容易滋生bug
- 只能再回調(diào)里處理異常
我們需要一種更同步抓半、更順序、更阻塞的方式來表達(dá)異步格嘁,就像我們的大腦一樣笛求。
-
控制反轉(zhuǎn) -> 信任問題
A和B發(fā)生于現(xiàn)在,在JavaScript主程序的直接控制之下糕簿,而C會(huì)延遲到將來發(fā)生探入,并且是在第三方的控制下(多數(shù)情況下,是某個(gè)第三方提供的工具)懂诗。這種控制的轉(zhuǎn)移通常不會(huì)給程序帶來很多問題蜂嗽。
我們用回調(diào)函數(shù)來封裝程序中的continuation,然后把回調(diào)交給第三方(甚至可能是外部代碼)殃恒,接著期待其能夠調(diào)用回調(diào)植旧,實(shí)現(xiàn)正確的功能。這種稱為 控制反轉(zhuǎn) 离唐,就是把自己程序一部分的執(zhí)行控制交給某個(gè)第三方病附。第三方提供某個(gè)工具,你傳入回調(diào)處理自己的邏輯亥鬓,由于你的代碼和第三方工具之間沒有一份明確表達(dá)的契約完沪,他們調(diào)用你的回調(diào)時(shí)可能出現(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)的錯(cuò)誤或異常
// A
ajax("..", function(){
// C
});
// B
對于被傳給你無法信任的工具的每個(gè)問題,你都將不得不創(chuàng)建大量的混亂邏輯嵌戈,此時(shí)是否更加明白回調(diào)地獄是多像地獄了吧覆积!
回調(diào)最大的問題是控制反轉(zhuǎn),它會(huì)導(dǎo)致信任鏈的完全斷裂咕别!
回調(diào)的變體
回調(diào)設(shè)計(jì)存在幾個(gè)變體技健,意在解決前面討論的一些信任問題(不是全部P囱ā)
分離回調(diào)
為了更優(yōu)雅地處理錯(cuò)誤惰拱,有些API設(shè)計(jì)提供了 分離回調(diào)(一個(gè)用于成功通知,一個(gè)用于出錯(cuò)通知)
function success(data){
console.log(data);
}
function failure(err){
console.error(err);
}
ajax("http://some.url.1", success, failure);
這種設(shè)計(jì)啊送,API的出錯(cuò)處理函數(shù) failure() 常常是可選的偿短,如果沒有提供的話,就是假定這個(gè)錯(cuò)誤可以吞掉馋没。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)的第一個(gè)參數(shù)保留用作錯(cuò)誤對象婆排。如果成功的話,這個(gè)參數(shù)就會(huì)被清空/置假(后續(xù)的參數(shù)就是成功數(shù)據(jù))笔链。如果產(chǎn)生了錯(cuò)誤結(jié)果段只,第一個(gè)參數(shù)就會(huì)被置起/置真(通常就不會(huì)再傳遞其他結(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)的問題≡拚恚現(xiàn)在事情更糟糕,因?yàn)楝F(xiàn)在你可能同時(shí)得到成功或失敗的結(jié)果坪创,或者都沒有炕婶,并且你還不得不編碼處理這些情況。
盡管這是可采用的標(biāo)準(zhǔn)模式莱预,但更加冗長和模式化柠掂,可復(fù)用性不高,還得給應(yīng)用中的每個(gè)回調(diào)添加這樣的代碼依沮。
??? 如何解決完全不調(diào)用的信任問題陪踩?設(shè)置一個(gè)超時(shí)來取消事件
??? 如何解決調(diào)用過早的信任問題?永遠(yuǎn)要異步悉抵,創(chuàng)建一個(gè)類似于驗(yàn)證概念版本的asyncify()工具
雖然可以寫一些特點(diǎn)邏輯來解決這些信任問題肩狂,但其難度高于應(yīng)有的水平,可能會(huì)產(chǎn)生更笨重姥饰、更難維護(hù)的代碼傻谁,并且缺少足夠的保護(hù),其中的損害要直到你受到bug的影響才會(huì)被發(fā)現(xiàn)列粪。
我們需要一個(gè)通用的方案來解決這些信任問題审磁。不管我們創(chuàng)建多少回調(diào),這一方案都應(yīng)可以復(fù)用岂座,且沒有重復(fù)代碼的開銷态蒂。
異常處理
try…catch是同步代碼,只能捕獲“同步代碼”中的"運(yùn)行時(shí)異常"费什,"同步代碼"是無法獲取如setTimeout钾恢、Promise等異步代碼的異常。
Q:為什么 try...catch
無法直接捕獲異步的錯(cuò)誤鸳址?
比如執(zhí)行 fs.readdir
的時(shí)候瘩蚪,其實(shí)是將回調(diào)函數(shù)加入任務(wù)隊(duì)列中,代碼繼續(xù)執(zhí)行稿黍,直至主線程完成后疹瘦,才會(huì)從任務(wù)隊(duì)列中選擇已完成的任務(wù),并將其加入棧中巡球,此時(shí)棧中只有這一個(gè)執(zhí)行上下文言沐,如果回調(diào)報(bào)錯(cuò)邓嘹,也無法獲取調(diào)用該異步操作時(shí)的棧中的信息,不容易判定哪里出現(xiàn)了錯(cuò)誤险胰。
因此吴超,要處理 setTimeout 等回調(diào)內(nèi)部的異常,只能將 try-catch
放置到回調(diào)內(nèi)部鸯乃。