第二章: 回調

特別說明,為便于查閱,文章轉自https://github.com/getify/You-Dont-Know-JS

在第一章中,我們探討了JavaScript中關于異步編程的術語和概念。我們的焦點是理解驅動所有“事件”(異步函數調用)的單線程(一次一個)事件輪詢隊列。我們還探討了各種解釋 同時 運行的事件鏈,或“進程”(任務, 函數調用等)間的關系的并發模式。

我們在第一章的所有例子中,將函數作為獨立的,不可分割的操作單位使用,在這些函數內部語句按照可預知的順序運行(在編譯器水平之上?。?,但是在函數順序水平上,事件(也就是異步函數調用)可以以各種順序發生。

在所有這些情況中,函數都是一個“回調”。因為無論什么時候事件輪詢隊列中的事件被處理時,這個函數都作為事件輪詢“調用并返回”程序的目標。

正如你觀察到的,在JS程序中,回調是到目前為止最常見的表達和管理異步的方式。確實,在JavaScript語言中回調是最基礎的異步模式。

無數的JS程序,即便是最精巧最復雜的程序,都曾經除了回調外不依靠任何其他異步模式而編寫(當然,和我們在第一章中探討的并發互動模式一起)?;卣{函數是JavaScript的異步苦工,而且它工作得相當好。

除了……回調并不是沒有缺點。許多開發者都對 Promises 提供的更好的異步模式感到興奮不已。但是如果你不明白它在抽象什么,和為什么抽象,是不可能有效利用任何抽象機制的。

在本章中,我們將深入探討這些話題,來說明為什么更精巧的異步模式(在本書的后續章節中探討)是必要和被期望的。

延續

讓我們回到在第一章中開始的異步回調的例子,但讓我稍微修改它一下來畫出重點:

// A
ajax( "..", function(..){
    // C
} );
// B

// A// B代表程序的前半部分(也就是 現在),// C標識了程序的后半部分(也就是 稍后)。前半部分立即執行,然后會出現一個不知多久的“暫停”。在未來某個時刻,如果Ajax調用完成了,那么程序會回到它剛才離開的地方,并 繼續 執行后半部分。

換句話說,回調函數包裝或封裝了程序的 延續。

讓我們把代碼弄得更簡單一些:

// A
setTimeout( function(){
    // C
}, 1000 );
// B

稍停片刻然后問你自己,你將如何描述(給一個不那么懂JS工作方式的人)這個程序的行為。來吧,大聲說出來。這個很好的練習將使我的下一個觀點更鮮明。

現在大多數讀者可能在想或說著這樣的話:“做A,然后設置一個等待1000毫秒的定時器,一旦它觸發,就做C”。與你的版本有多接近?

你可能已經發覺了不對勁兒的地方,給了自己一個修正版:“做A,設置一個1000毫秒的定時器,然后做B,然后在超時事件觸發后,做C”。這比第一個版本更準確。你能發現不同之處嗎?

雖然第二個版本更準確,但是對于以一種將我們的大腦匹配代碼,代碼匹配JS引擎的方式講解這段代碼來說,這兩個版本都是不足的。這里的鴻溝既是微小的也是巨大的,而且是理解回調作為異步表達和管理的缺點的關鍵。

只要我們以回調函數的方式引入一個延續(或者像許多程序員那樣引入幾十個?。覀兙驮试S了一個分歧在我們的大腦如何工作和代碼將運行的方式之間形成。當這兩者背離時,我們的代碼就不可避免地陷入這樣的境地:更難理解,更難推理,更難調試,和更難維護。

順序的大腦

我相信大多數讀者都曾經聽某個人說過(甚至你自己就曾這么說),“我能一心多用”。試圖表現得一心多用的效果包含幽默(孩子們的拍頭揉肚子游戲),平常的行為(邊走邊嚼口香糖),和徹頭徹尾的危險(開車時發微信)。

但我們是一心多用的人嗎?我們真的能執行兩個意識,有意地一起行動并在完全同一時刻思考/推理它們兩個嗎?我們最高級的大腦功能有并行的多線程功能嗎?

答案可能令你吃驚:可能不是這樣。

我們的大腦其實就不是這樣構成的。我們中大多數人(特別是A型人格?。┒际亲约翰磺樵赋姓J的一個一心一用者。其實我們只能在任一給定的時刻考慮一件事情。

我不是說我們所有的下意識,潛意識,大腦的自動功能,比如心跳,呼吸,和眨眼。那些都是我們延續生命的重要任務,我們不會有意識地給它們分配大腦的能量。謝天謝地,當我們在3分鐘內第15次刷朋友圈時,我們的大腦在后臺(線程?。├^續著這些重要任務。

相反我們討論的是在某時刻我們的意識最前線的任務。對我來說,是現在正在寫這本書。我還在這完全同一個時刻做其他高級的大腦活動嗎?不,沒有。我很快而且容易分心——在這最后的幾段中有幾十次了!

當我們 模擬 一心多用時,比如試著在打字的同時和朋友或家人通電話,實際上我們表現得更像一個快速環境切換器。換句話說,我們快速交替地在兩個或更多任務間來回切換,在微小,快速的區塊中 同時 處理每個任務。我們做的是如此之快,以至于從外界看開我們在 平行地 做這些事情。

難道這聽起來不像異步事件并發嗎(就像JS中發生的那樣)?!如果不,回去再讀一遍第一章!

事實上,將龐大復雜的神經內科世界簡化為我希望可以在這里討論的東西的一個方法是,我們的大腦工作起來有點兒像事件輪詢隊列。

如果你把我打得每一個字(或詞)當做一個單獨的異步事件,那么現在這一句話上就有十幾處地方,可以讓我的大腦被其他的事件打斷,比如我的感覺,甚至只是我隨機的想法。

我不會在每個可能的地方被打斷并被拉到其他的“處理”上去(謝天謝地——要不這本書永遠也寫不完了!)。但是它發生得也足夠頻繁,以至于我感到我的大腦幾乎持續不斷地切換到各種不同的環境(也就是“進程”)。而且這和JS引擎可能會感覺到的十分相像。

執行與計劃

好了,這么說來我們的大腦可以被認為是運行在一個單線程事件輪詢隊列中,就像JS引擎那樣。這聽起來是個不錯的匹配。

但是我們需要比我們剛才分析的更加細致入微。在我們如何計劃各種任務,和我們的大腦實際如何運行這些任務之間,有一個巨大,明顯的不同。

再一次,回到這篇文章的寫作的比擬上來。在我心里的粗略計劃輪廓是繼續寫啊寫,順序地經過一系列在我思想中定好的點。我沒有在這次寫作期間計劃任何的打擾或非線性的活動。但無論如何,我的大腦依然一直不停地切換。

即便在操作級別上我們的大腦是異步事件的,但我們還是用一種順序的,同步的方式計劃任務?!拔业萌ド痰辏缓筚I些牛奶,然后去干洗店”。

你會注意到這種高級思維(規劃)方式看起來不是那么“異步”。事實上,我們幾乎很少會故意只用事件的形式思考。相反,我們小心,順序地(A然后B然后C)計劃,而且我們假設一個區間有某種臨時的阻塞迫使B等待A,使C等待B。

當開發者編寫代碼時,他們規劃一組將要發生的動作。如果他們是合格的開發者,他們會 小心地規劃。比如“我需要將z的值設為x的值,然后將x的值設為y的值”。

當我們編寫同步代碼時,一個語句接一個語句,它工作起來就像我們的跑腿todo清單:

// 交換`x`與`y`(通過臨時變量`z`)
z = x;
x = y;
y = z;

這三個賦值語句是同步的,所以x=y會等待z=x完成,而y=z會相應地等待x=y完成。另一種說法是這三個語句臨時地按照特定的順序綁在一起執行,一個接一個。幸好我們不必在這里關心任何異步事件的細節。如果我們關心,代碼很快就會變得非常復雜!

如果同步的大腦規劃和同步的代碼語句匹配的很好,那么我們的大腦能把異步代碼規劃得多好呢?

事實證明,我們在代碼中表達異步的方式(用回調)和我們同步的大腦規劃行為根本匹配的不是很好。

你能實際想象一下像這樣規劃你的跑腿todo清單的思維線索嗎?

“我得去趟商店,但是我確信在路上我會接到一個電話,于是‘嗨,媽媽’,然后她開始講話,我會在GPS上搜索商店的位置,但那會花幾分鐘加載,所以我把收音機音量調小以便聽到媽媽講話,然后我發現我忘了穿夾克而且外面很冷,但沒關系,繼續開車并和媽媽說話,然后安全帶警報提醒我要系好,于是‘是的,媽,我系著安全帶呢,我總是系著安全帶!’。啊,GPS終于得到方向了,現在……”

雖然作為我們如何度過自己的一天,思考以什么順序做什么事的規劃聽起來很荒唐,但這正是我們大腦在功能層面運行的方式。記住,這不是一心多用,而只是快速的環境切換。

我們這些開發者編寫異步事件代碼困難的原因,特別是當我們只有回調手段可用時,就是意識思考/規劃的流動對我們大多數人是不自然的。

我們用一步接一步的方式思考,但是一旦我們從同步走向異步,在代碼中可以用的工具(回調)不是以一步接一步的方式表達的。

而且這就是為什么正確編寫和推理使用回調的異步JS代碼是如此困難:因為它不是我們的大腦進行規劃的工作方式。

注意: 唯一比不知道為什么代碼不好用更糟糕的是,從一開始就不知道為什么代碼好用!這是一種經典的“紙牌屋”心理:“它好用,但不知為什,所以大家都別碰!”你可能聽說過,“他人即地獄”(薩特),而程序員們模仿這種說法,“他人的代碼即地獄”。我相信:“不明白我自己的代碼才是地獄?!倍卣{正是肇事者之一。

嵌套/鏈接的回調

考慮下面的代碼:

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) ;
} );

你很可能一眼就能認出這樣的代碼。我們得到了三個嵌套在一起的函數鏈,每一個函數都代表異步序列(任務,“進程”)的一個步驟。

這樣的代碼常被稱為“回調地獄(callback hell)”,有時也被稱為“末日金字塔(pyramid of doom)”(由于嵌套的縮進使它看起來像一個放倒的三角形)。

但是“回調地獄”實際上與嵌套/縮進幾乎無關。它是一個深刻得多的問題。我們將繼續在本章剩下的部分看到它為什么和如何成為一個問題。

首先,我們等待“click”事件,然后我們等待定時器觸發,然后我們等待Ajax應答回來,就在這時它可能會將所有這些再做一遍。

猛地一看,這段代碼的異步性質可能看起來與順序的大腦規劃相匹配。

首先(現在),我們:

listen( "..", function handler(..){
    // ..
} );

稍后,我們:

setTimeout( function request(..){
    // ..
}, 500) ;

稍后,我們:

ajax( "..", function response(..){
    // ..
} );

最后(最 稍后),我們:

if ( .. ) {
    // ..
}
else ..

不過用這樣的方式線性推導這段代碼有幾個問題。

首先,這個例子中我們的步驟在一條順序的線上(1,2,3,和4……)是一個巧合。在真實的異步JS程序中,經常會有很多噪音把事情搞亂,在我們從一個函數跳到下一個函數時不得不在大腦中把這些噪音快速地演練一遍。理解這樣滿載回調的異步流程不是不可能,但絕不自然或容易,即使是經歷了很多練習后。

而且,有些更深層的,只是在這段代碼中不明顯的東西搞錯了。讓我們建立另一個場景(假想代碼)來展示它:

doA( function(){
    doB();

    doC( function(){
        doD();
    } )

    doE();
} );

doF();

雖然根據經驗你將正確地指出這些操作的真實順序,但我打賭它第一眼看上去有些使人糊涂,而且需要一些協調的思維周期才能搞明白。這些操作將會以這種順序發生:

  • doA()
  • doF()
  • doB()
  • doC()
  • doE()
  • doD()

你是在第一次瀏覽這段代碼就看明白的嗎?

好吧,你們肯定有些人在想我在函數的命名上不公平,故意引導你誤入歧途。我發誓我只是按照從上到下出現的順序命名的。不過讓我再試一次:

doA( function(){
    doC();

    doD( function(){
        doF();
    } )

    doE();
} );

doB();

現在,我以他們實際執行的順序用字母命名了。但我依然要打賭,即便是現在對這個場景有經驗的情況下,大多數讀者追蹤A -> B -> C -> D -> E -> F的順序并不是自然而然的。你的眼睛肯定在這段代碼中上上下下跳了許多次,對吧?

就算它對你來說都是自然的,這里依然還有一個可能肆虐的災難。你能發現它是什么嗎?

如果doA(..)doD(..)實際上不是如我們明顯地假設的那樣,不是異步的呢?嗯,現在順序不同了。如果它們都是同步的(也許僅僅有時是這樣,根據當時程序所處的條件而定),現在的順序是A -> C -> D -> F -> E -> B。

你在背景中隱約聽到的聲音,正是成千上萬雙手掩面的JS開發者的嘆息。

嵌套是問題嗎?是它使追蹤異步流程變得這么困難嗎?當然,有一部分是。

但是讓我不用嵌套重寫一遍前面事件/超時/Ajax嵌套的例子:

listen( "click", handler );

function handler() {
    setTimeout( request, 500 );
}

function request(){
    ajax( "http://some.url.1", response );
}

function response(text){
    if (text == "hello") {
        handler();
    }
    else if (text == "world") {
        request();
    }
}

這樣的代碼組織形式幾乎看不出來有前一種形式的嵌套/縮進困境,但它的每一處依然容易受到“回調地獄”的影響。為什么呢?

當我們線性地(順序地)推理這段代碼,我們不得不從一個函數跳到下一個函數,再跳到下一個函數,并在代碼中彈來彈去以“看到”順序流。并且要記住,這個簡化的代碼風格是某種最佳情況。我們都知道真實的JS程序代碼經常更加神奇地錯綜復雜,使這樣量級的順序推理更加困難。

另一件需要注意的事是:為了將第2,3,4步鏈接在一起使他們相繼發生,回調獨自給我們的啟示是將第2步硬編碼在第1步中,將第3步硬編碼在第2步中,將第4步硬編碼在第3步中,如此繼續。硬編碼不一定是一件壞事,如果第2步應當總是在第3步之前真的是一個固定條件。

不過硬編碼絕對會使代碼變得更脆弱,因為它不考慮任何可能使在步驟前行的過程中出現偏差的異常情況。舉個例子,如果第2步失敗了,第3步永遠不會到達,第2步也不會重試,或者移動到一個錯誤處理流程上,等等。

所有這些問題你都 可以 手動硬編碼在每一步中,但那樣的代碼總是重復性的,而且不能在其他步驟或你程序的其他異步流程中復用。

即便我們的大腦可能以順序的方式規劃一系列任務(這個,然后這個,然后這個),但我們大腦運行的事件的性質,使恢復/重試/分流這樣的流程控制幾乎毫不費力。如果你出去購物,而且你發現你把購物單忘在家里了,這并不會因為你沒有提前計劃這種情況而結束這一天。你的大腦會很容易地繞過這個小問題:你回家,取購物單,然后回頭去商店。

但是手動硬編碼的回調(甚至帶有硬編碼的錯誤處理)的脆弱本性通常不那么優雅。一旦你最終指明了(也就是提前規劃好了)所有各種可能性/路徑,代碼就會變得如此復雜以至于幾乎不能維護或更新。

才是“回調地獄”想表達的!嵌套/縮進基本上一個余興表演,轉移注意力的東西。

如果以上這些還不夠,我們還沒有觸及兩個或更多這些回調延續的鏈條 同時 發生會怎么樣,或者當第三步分叉成為帶有大門或門閂的“并行”回調,或者……我的天哪,我腦子疼,你呢?

你抓住這里的重點了嗎?我們順序的,阻塞的大腦規劃行為和面向回調的異步代碼不能很好地匹配。這就是需要清楚地闡明的關于回調的首要缺陷:它們在代碼中表達異步的方式,是需要我們的大腦不得不斗爭才能保持一致的。

信任問題

在順序的大腦規劃和JS代碼中回調驅動的異步處理間的不匹配只是關于回調的問題的一部分。還有一些更深刻的問題值得擔憂。

讓我們再一次重溫這個概念——回調函數是我們程序的延續(也就是程序的第二部分):

// A
ajax( "..", function(..){
    // C
} );
// B

// A// B現在 發生,在JS主程序的直接控制之下。但是// C被推遲到 稍后 再發生,并且在另一部分的控制之下——這里是ajax(..)函數。在基本的感覺上,這樣的控制交接一般不會讓程序產生很多問題。

但是不要被這種控制切換不是什么大事的罕見情況欺騙了。事實上,它是回調驅動的設計的最可怕的(也是最微妙的)問題。這個問題圍繞著一個想法展開:有時ajax(..)(或者說你向之提交回調的部分)不是你寫的函數,或者不是你可以直接控制的函數。很多時候它是一個由第三方提供的工具。

當你把你程序的一部分拿出來并把它執行的控制權移交給另一個第三方時,我們稱這種情況為“控制倒轉”。在你的代碼和第三方工具之間有一個沒有明言的“契約”——一組你期望被維護的東西。

五個回調的故事

為什么這件事情很重要可能不是那么明顯。讓我們來構建一個夸張的場景來生動地描繪一下信任危機。

想象你是一個開發者,正在建造一個販賣昂貴電視的網站的結算系統。你已經將結算系統的各種頁面順利地制造完成。在最后一個頁面,當用戶點解“確定”購買電視時,你需要調用一個第三方函數(假如由一個跟蹤分析公司提供),以便使這筆交易能夠被追蹤。

你注意到它們提供的是某種異步追蹤工具,也許是為了最佳的性能,這意味著你需要傳遞一個回調函數。在你傳入的這個程序的延續中,有你最后的代碼——劃客人的信用卡并顯示一個感謝頁面。

這段代碼可能看起來像這樣:

analytics.trackPurchase( purchaseData, function(){
    chargeCreditCard();
    displayThankyouPage();
} );

足夠簡單,對吧?你寫好代碼,測試它,一切正常,然后你把它部署到生產環境。大家都很開心!

6個月過去了,沒有任何問題。你幾乎已經忘了你曾寫過的代碼。一天早上,工作之前你先在咖啡店坐坐,悠閑地享用著你的拿鐵,直到你接到老板慌張的電話要求你立即扔掉咖啡并沖進辦公室。

當你到達時,你發現一位高端客戶為了買同一臺電視信用卡被劃了5次,而且可以理解,他不高興??头呀浀懒饲覆㈤_始辦理退款。但你的老板要求知道這是怎么發生的?!拔覀儧]有測試過這樣的情況嗎?。俊?/p>

你甚至不記得你寫過的代碼了。但你還是往回挖掘試著找出是什么出錯了。

在分析過一些日志之后,你得出的結論是,唯一的解釋是分析工具不知怎么的,由于某些原因,將你的回調函數調用了5次而非一次。他們的文檔中沒有任何東西提到此事。

十分令人沮喪,你聯系了客戶支持,當然他們和你一樣驚訝。他們同意將此事向上提交至開發者,并許諾給你回復。第二天,你收到一封很長的郵件解釋他們發現了什么,然后你將它轉發給了你的老板。

看起來,分析公司的開發者曾經制作了一些實驗性的代碼,在一定條件下,將會每秒重試一次收到的回調,在超時之前共計5秒。他們從沒想要把這部分推到生產環境,但不知怎地他們這樣做了,而且他們感到十分難堪而且抱歉。然后是許多他們如何定位錯誤的細節,和他們將要如何做以保證此事不再發生。等等,等等。

后來呢?

你找你的老板談了此事,但是他對事情的狀態不是感覺特別舒服。他堅持,而且你也勉強地同意,你不能再相信 他們 了(咬到你的東西),而你將需要指出如何保護放出的代碼,使它們不再受這樣的漏洞威脅。

修修補補之后,你實現了一些如下的特殊邏輯代碼,團隊中的每個人看起來都挺喜歡:

var tracked = false;

analytics.trackPurchase( purchaseData, function(){
    if (!tracked) {
        tracked = true;
        chargeCreditCard();
        displayThankyouPage();
    }
} );

注意: 對讀過第一章的你來說這應當很熟悉,因為我們實質上創建了一個門閂來處理我們的回調被并發調用多次的情況。

但一個QA的工程師問,“如果他們沒調你的回調怎么辦?” 噢。誰也沒想過。

你開始布下天羅地網,考慮在他們調用你的回調時所有出錯的可能性。這里是你得到的分析工具可能不正常運行的方式的大致列表:

  • 調用回調過早(在它開始追蹤之前)
  • 調用回調過晚 (或不調)
  • 調用回調太少或太多次(就像你遇到的問題!)
  • 沒能向你的回調傳遞必要的環境/參數
  • 吞掉了可能發生的錯誤/異常
  • ...

這感覺像是一個麻煩清單,因為它就是。你可能慢慢開始理解,你將要不得不為 每一個傳遞到你不能信任的工具中的回調 都創造一大堆的特殊邏輯。

現在你更全面地理解了“回調地獄”有多地獄。

不僅是其他人的代碼

現在有些人可能會懷疑事情到底是不是如我所宣揚的這么大條。也許你根本就不和真正的第三方工具互動。也許你用的是進行了版本控制的API,或者自己保管的庫,因此它的行為不會在你不知曉的情況下改變。

那么,好好思考這個問題:你能 真正 信任你理論上控制(在你的代碼庫中)的工具嗎?

這樣考慮:我們大多數人都同意,至少在某個區間內我們應當帶著一些防御性的輸入參數檢查制造我們自己的內部函數,來減少/防止以外的問題。

過于相信輸入:

function addNumbers(x,y) {
    // + 操作符使用強制轉換重載為字符串連接
    // 所以根據傳入參數的不同,這個操作不是嚴格的安全。
    return x + y;
}

addNumbers( 21, 21 );   // 42
addNumbers( 21, "21" ); // "2121"

防御不信任的輸入:

function addNumbers(x,y) {
    // 保證數字輸入
    if (typeof x != "number" || typeof y != "number") {
        throw Error( "Bad parameters" );
    }

    // 如果我們到達這里,+ 就可以安全地做數字加法
    return x + y;
}

addNumbers( 21, 21 );   // 42
addNumbers( 21, "21" ); // Error: "Bad parameters"

或者也許依然安全但更友好:

function addNumbers(x,y) {
    // 保證數字輸入
    x = Number( x );
    y = Number( y );

    // + 將會安全地執行數字加法
    return x + y;
}

addNumbers( 21, 21 );   // 42
addNumbers( 21, "21" ); // 42

不管你怎么做,這類函數參數的檢查/規范化是相當常見的,即便是我們理論上完全信任的代碼。用一個粗俗的說法,編程好像是地緣政治學的“信任但驗證”原則的等價物。

那么,這不是要推論出我們應當對異步函數回調的編寫做相同的事,而且不僅是針對真正的外部代碼,甚至要對一般認為是“在我們控制之下”的代碼?我們當然應該。

但是回調沒有給我們提供任何協助。我們不得不自己構建所有的裝置,而且這通常最終成為許多我們要在每個異步回調中重復的模板/負擔。

有關于回調的最麻煩的問題就是 控制反轉 導致所有這些信任完全崩潰。

如果你有代碼用到回調,特別是但不特指第三方工具,而且你還沒有為所有這些 控制反轉 的信任問題實施某些緩和邏輯,那么你的代碼現在就 bug,雖然它們還沒咬到你。將來的bug依然是bug。

確實是地獄。

嘗試拯救回調

有幾種回調的設計試圖解決一些(不是全部?。┪覀儎偛趴吹降男湃螁栴}。這是一種將回調模式從它自己的崩潰中拯救出來的勇敢,但注定失敗的努力。

舉個例子,為了更平靜地處理錯誤,有些API設計提供了分離的回調(一個用作成功的通知,一個用作錯誤的通知):

function success(data) {
    console.log( data );
}

function failure(err) {
    console.error( err );
}

ajax( "http://some.url.1", success, failure );

在這種設計的API中,failure()錯誤處理器通常是可選的,而且如果不提供的話它會假定你想讓錯誤被吞掉。呃。

注意: ES6的Promises的API使用的就是這種分離回調設計。我們將在下一章中詳盡地討論ES6的Promises。

另一種常見的回調設計模式稱為“錯誤優先風格”(有時稱為“Node風格”,因為它幾乎在所有的Node.js的API中作為慣例使用),一個回調的第一個參數為一個錯誤對象保留(如果有的話)。如果成功,這個參數將會是空/falsy(而其他后續的參數將是成功的數據),但如果出現了錯誤的結果,這第一個參數就會被設置/truthy(而且通常沒有其他東西會被傳遞了):

function response(err,data) {
    // 有錯?
    if (err) {
        console.error( err );
    }
    // 否則,認為成功
    else {
        console.log( data );
    }
}

ajax( "http://some.url.1", response );

這兩種方法都有幾件事情應當注意。

首先,它們沒有像看起來那樣真正解決主要的信任問題。在這兩個回調中沒有關于防止或過濾意外的重復調用的東西。而且,事情現在更糟糕了,因為你可能同時得到成功和失敗信號,或者都得不到,你仍然不得不圍繞著這兩種情況寫代碼。

還有,不要忘了這樣的事實:雖然它們是你可以引用的標準模式,但它們絕對更加繁冗,而且是不太可能復用的模板代碼,所以你將會對在你應用程序的每一個回調中敲出它們感到厭倦。

回調從不被調用的信任問題怎么解決?如果這要緊(而且它可能應當要緊!),你可能需要設置一個超時來取消事件。你可以制作一個工具來幫你:

function timeoutify(fn,delay) {
    var intv = setTimeout( function(){
            intv = null;
            fn( new Error( "Timeout!" ) );
        }, delay )
    ;

    return function() {
        // 超時還沒有發生?
        if (intv) {
            clearTimeout( intv );
            fn.apply( this, [ null ].concat( [].slice.call( arguments ) ) );
        }
    };
}

這是你如何使用它:

// 使用“錯誤優先”風格的回調設計
function foo(err,data) {
    if (err) {
        console.error( err );
    }
    else {
        console.log( data );
    }
}

ajax( "http://some.url.1", timeoutify( foo, 500 ) );

另一個信任問題是被調用的“過早”。在應用程序規范上講,這可能涉及在某些重要的任務完成之前被調用。但更一般地,在那些即可以 現在(同步地),也可以在 稍后(異步地)調用你提供的回調的工具中這個問題更明顯。

這種圍繞著同步或異步行為的不確定性,幾乎總是導致非常難追蹤的Bug。在某些圈子中,一個名叫Zalgo的可以導致人精神錯亂的虛構怪物被用來描述這種同步/異步的噩夢。經常能聽到人們喊“別放出Zalgo!”,而且它引出了一個非常響亮的建議:總是異步地調用回調,即便它是“立即”在事件輪詢的下一個迭代中,這樣所有的回調都是可預見的異步。

注意: 更多關于Zalgo的信息,參見Oren Golan的“Don't Release Zalgo!(不要釋放Zalgo?。?https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md)和Isaac Z. Schlueter的“Designing APIs for Asynchrony(異步API設計)”(http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony)。

考慮下面的代碼:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;

這段代碼是打印0(同步回調調用)還是打印1(異步回調調用)?這……要看情況。

你可以看到Zalgo的不可預見性能有多快地威脅你的JS程序。所以聽起來傻呼呼的“別放出Zalgo”實際上是一個不可思議地常見且實在的建議——總是保持異步。

如果你不知道當前的API是否會總是異步地執行呢?你可以制造一個像asyncify(..)這樣的工具:

function asyncify(fn) {
    var orig_fn = fn,
        intv = setTimeout( function(){
            intv = null;
            if (fn) fn();
        }, 0 )
    ;

    fn = null;

    return function() {
        // 觸發太快,在`intv`計時器觸發來
        // 表示異步回合已經過去之前?
        if (intv) {
            fn = orig_fn.bind.apply(
                orig_fn,
                // 將包裝函數的`this`加入`bind(..)`調用的
                // 參數,同時currying其他所有的傳入參數
                [this].concat( [].slice.call( arguments ) )
            );
        }
        // 已經是異步
        else {
            // 調用原版的函數
            orig_fn.apply( this, arguments );
        }
    };
}

你像這樣使用asyncify(..):

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", asyncify( result ) );
a++;

不管Ajax請求是由于存在于緩存中而解析為立即調用回調,還是它必須走過網線去取得數據而異步地稍后完成,這段代碼總是輸出1而不是0——result(..)總是被異步地調用,這意味著a++有機會在result(..)之前運行。

噢耶,又一個信任問題被“解決了”!但它很低效,而且又有更多臃腫的模板代碼讓你的項目變得沉重。

這只是關于回調一遍又一遍地發生的故事。它們幾乎可以做任何你想做的事,但你不得不努力工作來達到目的,而且大多數時候這種努力比你應當在推理這樣的代碼上所付出的多得多。

你可能發現自己希望有一些內建的API或語言機制來解決這些問題。終于ES6帶著一個偉大的答案到來了,所以繼續讀下去!

復習

回調是JS中異步的基礎單位。但是隨著JS的成熟,它們對于異步編程的演化趨勢來講顯得不夠。

首先,我們的大腦用順序的,阻塞的,單線程的語義方式規劃事情,但是回調使用非線性,非順序的方式表達異步流程,這使我們正確推理這樣的代碼變得非常困難。不好推理的代碼是導致不好的Bug的不好的代碼。

我們需要一個種方法,以更同步化,順序化,阻塞的方式來表達異步,正如我們的大腦那樣。

第二,而且是更重要的,回調遭受著 控制反轉 的蹂躪,它們隱含地將控制權交給第三方(通常第三方工具不受你控制?。﹣碚{用你程序的 延續。這種控制權的轉移使我們得到一張信任問題的令人不安的列表,比如回調是否會比我們期望的被調用更多次。

制造特殊的邏輯來解決這些信任問題是可能的,但是它比它應有的難度高多了,還會產生更笨重和更難維護的代碼,而且在bug實際咬到你的時候代碼會顯得在這些危險上被保護的不夠。

我們需要一個 所有這些信任問題 的一般化解決方案。一個可以被所有我們制造的回調復用,而且沒有多余的模板代碼負擔的方案。

我們需要比回調更好的東西。目前為止它們做的不錯,但JavaScript的 未來 要求更精巧和強大的異步模式。本書的后續章節將會深入這些新興的發展變化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容