當(dāng)前位置: 首頁IT技術(shù) → Javascript的回調(diào)機(jī)制的經(jīng)典教程

Javascript的回調(diào)機(jī)制的經(jīng)典教程

更多

  由于其運(yùn)行環(huán)境的特殊性,Javascript大量使用異步的通信機(jī)制,凡是涉及到網(wǎng)絡(luò)調(diào)用和事件機(jī)制的代碼都會涉及。在異步通信的環(huán)境下編碼經(jīng)常會用到 回調(diào)函數(shù)。Javascript由于有函數(shù)式語言的一些特點(diǎn)使得它在Javascript里面實(shí)現(xiàn)回調(diào)函數(shù)非常的優(yōu)雅和自然,包括函數(shù)作為一級的對象、匿 名函數(shù)、閉包機(jī)制等。但是要體會到個中的優(yōu)雅,需要先融匯貫通這些機(jī)制。如果是初學(xué)者學(xué)習(xí)這些東西可能比有編程經(jīng)驗的人少很多障礙,認(rèn)為事情本來就該是這 個樣子。但是,對于長期使用過程式語言編碼(比如傳統(tǒng)的C/C++程序員),又沒有接觸過函數(shù)式語言的程序員來說,可能需要閱讀一道思維的小坎。這件事情 有時候會造成一定的困擾,因為“老手”程序員會想:畢竟我已經(jīng)懂得一套能寫程序的方法,大家都說語言之間差別不重要,畢竟C++里面也有使用異步調(diào)用的時 候,主要注意一下語法的區(qū)別就好了。所以最終就變成了使用Javascript來模仿別的過程式語言,這樣的結(jié)果最終很有可能是寫出很別扭的程序給自己添 堵。本文嘗試用幾個例子說明異步通信的環(huán)境用Javascript寫回調(diào)函數(shù)很使用類似C語言寫回調(diào)函數(shù)的區(qū)別,以及為什么Javascript原生要更 適合做這件事情。(簡單起見,下面例子中的代碼均為偽代碼,并不一定嚴(yán)格符合C/C++或者Javascript的語法,但是筆者盡量寫得與語法要求接 近。)

  

  我們首先從C/C++的同步調(diào)用開始,假設(shè)我們要寫一個函數(shù),向遠(yuǎn)方的服務(wù)器發(fā)送一個字符串形式得命令,并且從服務(wù)器得到一個字符串作為響應(yīng)。例1就展示了使用C語言在同步同步通信的機(jī)制下代碼的樣子。

  

  例1 使用C語言的編碼方式實(shí)現(xiàn)調(diào)用訪問遠(yuǎn)程的接口

  view plaincopy to clipboardprint?

  01.//{{{get_data_v1

  02.int get_data_v1()

  03.{

  04. // 準(zhǔn)備數(shù)據(jù)

  05. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  06. char bufRcv[4096];

  07. // 建立連接

  08. socket s = new Socket();

  09. connnect(s, ip, port);

  10. // 發(fā)送數(shù)據(jù)

  11. send(s, bufCmd);

  12. // 接收數(shù)據(jù)

  13. recv(s, bufRcv);

  14. // 處理結(jié)果

  15. use(bufRcv);

  16. return 0;

  17.}

  18.//}}}

  在 例1中,get_data_v1執(zhí)行了準(zhǔn)備數(shù)據(jù)、創(chuàng)建了socket、建立連接、發(fā)送請求、接收響應(yīng)并最終使用use函數(shù)處理接收到的數(shù)據(jù),一切都顯得很 自然。為了方便說明問題,我們將這個通信的過程封裝一下,將整個建立連接并收發(fā)包的過程封裝成一個叫send_and_recv的函數(shù)。

  

  例2 將通信過程封裝成獨(dú)立的函數(shù),簡化業(yè)務(wù)流程代碼

  view plaincopy to clipboardprint?

  01.//{{{get_data_v2

  02.// 發(fā)包收包的過程

  03.int send_and_recv(struct addr, char* bufCmd, char* bufRcv)

  04.{

  05. socket s = new Socket();

  06. connnect(s, addr.ip, addr.port);

  07. send(s, bufCmd);

  08. recv(s, bufRcv);

  09.}

  10.// 原來的業(yè)務(wù)流程

  11.int get_data_v2()

  12.{

  13. // 準(zhǔn)備數(shù)據(jù)

  14. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  15. char bufRecv[4096];

  16. // 通信,收發(fā)數(shù)據(jù)

  17. // addr={ip, port}

  18. send_and_recv(addr, bufCmd, bufRcv);

  19. // 處理結(jié)果

  20. use(bufRcv);

  21. return 0;

  22.}

  23.//}}}

  例 2和例1很類似,不過是對通信過程進(jìn)行封裝了,并且ip-port對也變成了一個叫addr的地址結(jié)構(gòu)體。改動以后處理過程變得更簡單,剩下準(zhǔn)備數(shù)據(jù)、通 信和處理結(jié)果三步,F(xiàn)在,我們開始進(jìn)入正題,現(xiàn)在我們假設(shè)這個通信過程變成異步的,它接收一個回調(diào)函數(shù)用于處理取得的數(shù)據(jù)。如例3所示。

  

  例3 將通信過程變成異步調(diào)用

  view plaincopy to clipboardprint?

  01.//{{{get_data_v3

  02.// 變成異步調(diào)用以后,原來的調(diào)用過程分成了兩段

  03.// 前半段組裝參數(shù)調(diào)用發(fā)包過程

  04.// 后半段處理返

  05.// 這里假設(shè)send_and_recv是一個異步的網(wǎng)絡(luò)通信函數(shù)

  06.void get_data_v3()

  07.{

  08. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  09. char bufRcv[4096];

  10. send_and_recv_async(addr, bufCmd, bufRcv, callback);

  11.} // end of get_data_v3

  12.// 回調(diào)函數(shù)的定義

  13.int callback(char* bufRcv) {

  14. // 處理接收都的數(shù)據(jù)

  15. use(bufRcv);

  16. return 0;

  17.}

  18.//}}}

  在 例3中,假設(shè)使用了一個異步的通信過程send_and_recv_async,最后一個參數(shù)callback是一個回調(diào)函數(shù)指針。然后,當(dāng)接收到響應(yīng)以 后,send_and_recv_async會調(diào)用callback并傳入接收到的數(shù)據(jù)。相比例2,這個get_data的過程被異步通信過程一分為二: 前半段為準(zhǔn)備請求,后半段是處理結(jié)果。事實(shí)上,對將同步通信方式變成異步以后,都會涉及到將原來完整處理過程一分為二的問題。在兩段程序沒有什么相互依賴 的情況下,這樣的分解不會造成什么問題。但是,如果處理結(jié)果的過程依賴于一些外部參數(shù),那么情況就會變得很復(fù)雜。我們先來看看在同步通信的情況下,程序的 樣子,見例4。

  

  例4 假設(shè)處理結(jié)果的時候依賴外部參數(shù)

  view plaincopy to clipboardprint?

  01.//{{{get_data_v4

  02.// 這里原來的業(yè)務(wù)流程需要外部傳進(jìn)來的兩個參數(shù)(a,b)來決定如何處理結(jié)果

  03.int get_data_v4(int a, int b)

  04.{

  05. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  06. char bufRcv[4096];

  07. send_and_recv(addr, bufCmd, bufRcv);

  08. // 處理過程依賴于外部傳進(jìn)來的參數(shù)a和b

  09. use(bufRcv, a, b);

  10. return 0;

  11.}

  12.//}}}

  在例4中,我們的結(jié)果處理過程use依賴于傳入的兩個參數(shù)a和b,F(xiàn)在我們來看看例4的程序如果使用異步通信會怎樣,見例5。

  

  例5 加上參數(shù)依賴后再變成異步調(diào)用

  view plaincopy to clipboardprint?

  01.// 版本a

  02.//{{{get_data_v5

  03.// 需要參數(shù)的異步調(diào)用需要將參數(shù)透傳到后半段的回調(diào)函數(shù)中

  04.void get_data_v5a(int a, int b)

  05.{

  06. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  07. char bufRcv[4096];

  08. send_and_recv_async(addr, bufCmd, bufRcv, callbacka, a, b);

  09.} // end of get_data_v5a

  10.// 回調(diào)函數(shù)的定義

  11.int callbacka(char* bufRcv, int a, int b) {

  12. use(bufRcv, a, b);

  13. return 0;

  14.}

  15.// 版本b

  16.int g_a;

  17.int g_b;

  18.void get_data_v5b(int a, int b)

  19.{

  20. g_a = a;

  21. g_b = b;

  22. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  23. char bufRcv[4096];

  24. send_and_recv_async(addr, bufCmd, bufRcv, callbackb);

  25.} // end of get_data_v5b

  26.// 回調(diào)函數(shù)的定義

  27.int callbacka(char* bufRcv, int a, int b) {

  28.int callbackb(char* bufRcv) {

  29. use(bufRcv, g_a, g_b);

  30. return 0;

  31.}

  32.//}}}

  例 5中有兩個版本,get_data_v5a假設(shè)了通信機(jī)制可以透傳a和b兩個參數(shù)給回調(diào)函數(shù),get_data_v5b則使用了兩個全局變量來傳遞處理結(jié) 果所需的參數(shù)。兩個都不見得是很好的方法,get_data_v5a的問題是,異步通信的機(jī)制不見得能提供這種透傳機(jī)制,除非程序員自己封裝;即使程序員 自己封裝,那也意味著如果要實(shí)現(xiàn)多個處理數(shù)據(jù)的過程(像get_data)那就要實(shí)現(xiàn)多個異步調(diào)用的過程(send_and_recv_async),代 碼復(fù)雜且復(fù)用性差不好維護(hù)。而全局變量的版本也好不到哪里去,使用這種全局的機(jī)制,意味著不必要的信息暴露,也就有被別的地方錯修改的問題,同時這個函數(shù) 還變成不可重入的。即使將全局機(jī)制封裝在一個類里面,每次初始化一個對象,可以改善依然不能解決信息暴露的問題,同時還帶來了管理這多個對象的復(fù)雜性。

  

  兩種方法相比而言,貌似透傳的機(jī)制要稍好一些。我們對get_data_v5a略做修改,使得它通信過程能夠有更廣泛的復(fù)用。

  

  例6 使用一個closure對象打包過程中的參數(shù)

  view plaincopy to clipboardprint?

  01.//{{{get_data_v6

  02.// 為了統(tǒng)一回調(diào)函數(shù)的形式并且縮短回調(diào)的參數(shù)列表,將這種需要透傳的參數(shù)只有一個

  03.// 統(tǒng)一的數(shù)據(jù)結(jié)構(gòu)打包

  04.void get_data_v6(int a, int b)

  05.{

  06. // 準(zhǔn)備數(shù)據(jù)

  07. char bufCmd[]="cmd=1001&uin=123456?m=abc";

  08. char bufRcv[4096];

  09. // 打包處理結(jié)果所需要的參數(shù)

  10. closure.a = a;

  11. closure.b = b;

  12. // 通信

  13. send_and_recv_async(addr, bufCmd, bufRcv, callback, closure);

  14.} // end of get_data_v6

  15.// 回調(diào)函數(shù)的定義

  16.int callback(char* bufRcv, struct closure) {

  17. // 處理結(jié)果

  18. use(bufRcv, closure.a, closure.b);

  19. return 0;

  20.}

  21.//}}}

  例 6里面使用了一個叫closure的結(jié)構(gòu),假設(shè)這個結(jié)構(gòu)是個通用的數(shù)據(jù)容器,可以容納我們使用的個中類型的任意數(shù)量的參數(shù)。增加了這一個萬能的數(shù)據(jù)容器參 數(shù)以后,異步通信過程只要能透傳這么一個數(shù)據(jù)容器就能夠很好支持個中各樣的參數(shù)透傳的需求。這個數(shù)據(jù)容器由于是在get_data函數(shù)內(nèi)部產(chǎn)生的局部變 量,不會污染全局?jǐn)?shù)據(jù)或者比get_data更大的作用域。這種受限的可見性不僅提高了代碼的可維護(hù)性,還恢復(fù)了函數(shù)的可重入性。

  

  至此我 們關(guān)于回調(diào)機(jī)制的實(shí)現(xiàn)的假想代碼可以說已經(jīng)達(dá)到比較優(yōu)雅的程度了,僅僅還有一朵小烏云。那就是我們忽略了C/C++語言里面并沒有原生實(shí)現(xiàn)這個超級結(jié)構(gòu), 同樣我們依然還有一點(diǎn)點(diǎn)麻煩就是還需要指定要透傳的參數(shù)?紤]到原本從準(zhǔn)備數(shù)據(jù)到通信再到處理結(jié)果是一個完整統(tǒng)一的過程,原本不需要區(qū)分什么數(shù)據(jù)是前半端 使用的什么數(shù)據(jù)是后半段使用的,只要腳氣怎么治療讓前半端和后半段共享一個上下文在大部分情況下就能滿足需求了。所以現(xiàn)實(shí)情況下我們只能做一些妥協(xié),使用個中折衷方案 來使得程序能運(yùn)行起來。同樣,考慮到回調(diào)函數(shù)和啟動函數(shù)的關(guān)系,給回調(diào)函數(shù)命名也不是那么優(yōu)雅的事情,因為畢竟它們只是同一個過程的兩半,卻要使用兩個名 字,合理一點(diǎn)就應(yīng)該叫g(shù)et_data_first和get_data_second,或者get_data_trigger和 get_data_result_handler。如果接口多的話,就會有很多這種某過程first和某過程second,或者某過程trigger和某 過程result_handler。能不能某過程就象同步那樣使用一個名字呢?我們的設(shè)想真的就沒有辦法達(dá)到嗎?答案是否定的,在Javascript能 夠幫助我們實(shí)現(xiàn)我們所有的設(shè)想。見例7。

  

  例7 Javascript的異步調(diào)用

  view plaincopy to clipboardprint?

  01.//{{{get_data_js

  02.//

  03.// 寫成Javascript代碼就變成現(xiàn)在這個樣子

  04.// url對應(yīng)之前的addr

  05.// 使用匿名函數(shù)代替原來命名的callback定義

  06.// 原生支持閉包c(diǎn)losure

  07.//

  08.function get_data_js(a, b)

  09.{

  10. var bufCmd = "cmd=1001&uin=123456?m=abc";

  11. var bufRcv;

  12. send_and_recv_with_xhr(/*addr*/url, bufCmd, bufRcv, /*callback*/

  13. function(bufRcv/*, closure*/) {

  14. use(bufRcv, /*closure.*/a, /*closure.*/b);

  15. return 0;

  16. }

  17. );

  18.}

  19.//}}}

  例 7是使用Javascript實(shí)現(xiàn)類似例6的功能,僅僅存在一些細(xì)微的差別。例6的場景下可能更多使用TCP或者UDP作為通信協(xié)議,而在例7使用的則是 瀏覽器提供的XHR對象實(shí)現(xiàn)的HTTP協(xié)議。這點(diǎn)差別并不會影響我們對于異步通信下回調(diào)函數(shù)實(shí)現(xiàn)機(jī)制的討論,只要他們的通信機(jī)制都是異步的就可以了。例7 中使用注釋的形式標(biāo)注了例6里面使用的一些參數(shù)的名字以暗示它們的對應(yīng)關(guān)系,方便比較這兩個例子。我們看到了,在Javascript里面我們所有的設(shè)想 都變成了現(xiàn)實(shí)。(1)首先關(guān)于能夠透傳一切的超級結(jié)構(gòu),Javascript中實(shí)現(xiàn)了閉包的機(jī)制,保證了在這種內(nèi)部的函數(shù)對象可以訪問到定義它的環(huán)境能訪 問到的所有數(shù)據(jù),也就是在例7中的匿名回調(diào)函數(shù)可以訪問到get_data_js中能訪問到的所有數(shù)據(jù)。當(dāng)然,這里重要的是局部數(shù)據(jù),如a和b。如果是全 局?jǐn)?shù)據(jù)的話左旋肉堿真的有用嗎并不需要通過閉包也能訪問到。而且這個過程是Javascript的運(yùn)行環(huán)境提供的,對于程序員是透明的,程序員并不需要指定哪些參數(shù)需要透 傳。(2)不需要再為回調(diào)函數(shù)命名,因為Javascript支持匿名函數(shù)的定義,可以像定義變量一樣定義函數(shù)。而這個最終導(dǎo)致了我們在使用異步通信機(jī)制 的時候和使用同步的通信機(jī)制及其接近,沒有多余的名字,沒有不必要的可見性。

熱門評論
最新評論
發(fā)表評論 查看所有評論(0)
昵稱:
表情: 高興 可 汗 我不要 害羞 好 下下下 送花 屎 親親
字?jǐn)?shù): 0/500 (您的評論需要經(jīng)過審核才能顯示)