相關資訊
本類常用軟件
-
福建農村信用社手機銀行客戶端下載下載量:584204
-
Windows優化大師下載量:416896
-
90美女秀(視頻聊天軟件)下載量:366961
-
廣西農村信用社手機銀行客戶端下載下載量:365699
-
快播手機版下載量:325855
由于其運行環境的特殊性,Javascript大量使用異步的通信機制,凡是涉及到網絡調用和事件機制的代碼都會涉及。在異步通信的環境下編碼經常會用到 回調函數。Javascript由于有函數式語言的一些特點使得它在Javascript里面實現回調函數非常的優雅和自然,包括函數作為一級的對象、匿 名函數、閉包機制等。但是要體會到個中的優雅,需要先融匯貫通這些機制。如果是初學者學習這些東西可能比有編程經驗的人少很多障礙,認為事情本來就該是這 個樣子。但是,對于長期使用過程式語言編碼(比如傳統的C/C++程序員),又沒有接觸過函數式語言的程序員來說,可能需要閱讀一道思維的小坎。這件事情 有時候會造成一定的困擾,因為“老手”程序員會想:畢竟我已經懂得一套能寫程序的方法,大家都說語言之間差別不重要,畢竟C++里面也有使用異步調用的時 候,主要注意一下語法的區別就好了。所以最終就變成了使用Javascript來模仿別的過程式語言,這樣的結果最終很有可能是寫出很別扭的程序給自己添 堵。本文嘗試用幾個例子說明異步通信的環境用Javascript寫回調函數很使用類似C語言寫回調函數的區別,以及為什么Javascript原生要更 適合做這件事情。(簡單起見,下面例子中的代碼均為偽代碼,并不一定嚴格符合C/C++或者Javascript的語法,但是筆者盡量寫得與語法要求接 近。)
我們首先從C/C++的同步調用開始,假設我們要寫一個函數,向遠方的服務器發送一個字符串形式得命令,并且從服務器得到一個字符串作為響應。例1就展示了使用C語言在同步同步通信的機制下代碼的樣子。
例1 使用C語言的編碼方式實現調用訪問遠程的接口
view plaincopy to clipboardprint?
01.//{{{get_data_v1
02.int get_data_v1()
03.{
04. // 準備數據
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. // 發送數據
11. send(s, bufCmd);
12. // 接收數據
13. recv(s, bufRcv);
14. // 處理結果
15. use(bufRcv);
16. return 0;
17.}
18.//}}}
在 例1中,get_data_v1執行了準備數據、創建了socket、建立連接、發送請求、接收響應并最終使用use函數處理接收到的數據,一切都顯得很 自然。為了方便說明問題,我們將這個通信的過程封裝一下,將整個建立連接并收發包的過程封裝成一個叫send_and_recv的函數。
例2 將通信過程封裝成獨立的函數,簡化業務流程代碼
view plaincopy to clipboardprint?
01.//{{{get_data_v2
02.// 發包收包的過程
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.// 原來的業務流程
11.int get_data_v2()
12.{
13. // 準備數據
14. char bufCmd[]="cmd=1001&uin=123456?m=abc";
15. char bufRecv[4096];
16. // 通信,收發數據
17. // addr={ip, port}
18. send_and_recv(addr, bufCmd, bufRcv);
19. // 處理結果
20. use(bufRcv);
21. return 0;
22.}
23.//}}}
例 2和例1很類似,不過是對通信過程進行封裝了,并且ip-port對也變成了一個叫addr的地址結構體。改動以后處理過程變得更簡單,剩下準備數據、通 信和處理結果三步。現在,我們開始進入正題,現在我們假設這個通信過程變成異步的,它接收一個回調函數用于處理取得的數據。如例3所示。
例3 將通信過程變成異步調用
view plaincopy to clipboardprint?
01.//{{{get_data_v3
02.// 變成異步調用以后,原來的調用過程分成了兩段
03.// 前半段組裝參數調用發包過程
04.// 后半段處理返
05.// 這里假設send_and_recv是一個異步的網絡通信函數
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.// 回調函數的定義
13.int callback(char* bufRcv) {
14. // 處理接收都的數據
15. use(bufRcv);
16. return 0;
17.}
18.//}}}
在 例3中,假設使用了一個異步的通信過程send_and_recv_async,最后一個參數callback是一個回調函數指針。然后,當接收到響應以 后,send_and_recv_async會調用callback并傳入接收到的數據。相比例2,這個get_data的過程被異步通信過程一分為二: 前半段為準備請求,后半段是處理結果。事實上,對將同步通信方式變成異步以后,都會涉及到將原來完整處理過程一分為二的問題。在兩段程序沒有什么相互依賴 的情況下,這樣的分解不會造成什么問題。但是,如果處理結果的過程依賴于一些外部參數,那么情況就會變得很復雜。我們先來看看在同步通信的情況下,程序的 樣子,見例4。
例4 假設處理結果的時候依賴外部參數
view plaincopy to clipboardprint?
01.//{{{get_data_v4
02.// 這里原來的業務流程需要外部傳進來的兩個參數(a,b)來決定如何處理結果
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. // 處理過程依賴于外部傳進來的參數a和b
09. use(bufRcv, a, b);
10. return 0;
11.}
12.//}}}
在例4中,我們的結果處理過程use依賴于傳入的兩個參數a和b。現在我們來看看例4的程序如果使用異步通信會怎樣,見例5。
例5 加上參數依賴后再變成異步調用
view plaincopy to clipboardprint?
01.// 版本a
02.//{{{get_data_v5
03.// 需要參數的異步調用需要將參數透傳到后半段的回調函數中
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.// 回調函數的定義
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.// 回調函數的定義
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假設了通信機制可以透傳a和b兩個參數給回調函數,get_data_v5b則使用了兩個全局變量來傳遞處理結 果所需的參數。兩個都不見得是很好的方法,get_data_v5a的問題是,異步通信的機制不見得能提供這種透傳機制,除非程序員自己封裝;即使程序員 自己封裝,那也意味著如果要實現多個處理數據的過程(像get_data)那就要實現多個異步調用的過程(send_and_recv_async),代 碼復雜且復用性差不好維護。而全局變量的版本也好不到哪里去,使用這種全局的機制,意味著不必要的信息暴露,也就有被別的地方錯修改的問題,同時這個函數 還變成不可重入的。即使將全局機制封裝在一個類里面,每次初始化一個對象,可以改善依然不能解決信息暴露的問題,同時還帶來了管理這多個對象的復雜性。
兩種方法相比而言,貌似透傳的機制要稍好一些。我們對get_data_v5a略做修改,使得它通信過程能夠有更廣泛的復用。
例6 使用一個closure對象打包過程中的參數
view plaincopy to clipboardprint?
01.//{{{get_data_v6
02.// 為了統一回調函數的形式并且縮短回調的參數列表,將這種需要透傳的參數只有一個
03.// 統一的數據結構打包
04.void get_data_v6(int a, int b)
05.{
06. // 準備數據
07. char bufCmd[]="cmd=1001&uin=123456?m=abc";
08. char bufRcv[4096];
09. // 打包處理結果所需要的參數
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.// 回調函數的定義
16.int callback(char* bufRcv, struct closure) {
17. // 處理結果
18. use(bufRcv, closure.a, closure.b);
19. return 0;
20.}
21.//}}}
例 6里面使用了一個叫closure的結構,假設這個結構是個通用的數據容器,可以容納我們使用的個中類型的任意數量的參數。增加了這一個萬能的數據容器參 數以后,異步通信過程只要能透傳這么一個數據容器就能夠很好支持個中各樣的參數透傳的需求。這個數據容器由于是在get_data函數內部產生的局部變 量,不會污染全局數據或者比get_data更大的作用域。這種受限的可見性不僅提高了代碼的可維護性,還恢復了函數的可重入性。
至此我 們關于回調機制的實現的假想代碼可以說已經達到比較優雅的程度了,僅僅還有一朵小烏云。那就是我們忽略了C/C++語言里面并沒有原生實現這個超級結構, 同樣我們依然還有一點點麻煩就是還需要指定要透傳的參數。考慮到原本從準備數據到通信再到處理結果是一個完整統一的過程,原本不需要區分什么數據是前半端 使用的什么數據是后半段使用的,只要腳氣怎么治療讓前半端和后半段共享一個上下文在大部分情況下就能滿足需求了。所以現實情況下我們只能做一些妥協,使用個中折衷方案 來使得程序能運行起來。同樣,考慮到回調函數和啟動函數的關系,給回調函數命名也不是那么優雅的事情,因為畢竟它們只是同一個過程的兩半,卻要使用兩個名 字,合理一點就應該叫get_data_first和get_data_second,或者get_data_trigger和 get_data_result_handler。如果接口多的話,就會有很多這種某過程first和某過程second,或者某過程trigger和某 過程result_handler。能不能某過程就象同步那樣使用一個名字呢?我們的設想真的就沒有辦法達到嗎?答案是否定的,在Javascript能 夠幫助我們實現我們所有的設想。見例7。
例7 Javascript的異步調用
view plaincopy to clipboardprint?
01.//{{{get_data_js
02.//
03.// 寫成Javascript代碼就變成現在這個樣子
04.// url對應之前的addr
05.// 使用匿名函數代替原來命名的callback定義
06.// 原生支持閉包closure
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實現類似例6的功能,僅僅存在一些細微的差別。例6的場景下可能更多使用TCP或者UDP作為通信協議,而在例7使用的則是 瀏覽器提供的XHR對象實現的HTTP協議。這點差別并不會影響我們對于異步通信下回調函數實現機制的討論,只要他們的通信機制都是異步的就可以了。例7 中使用注釋的形式標注了例6里面使用的一些參數的名字以暗示它們的對應關系,方便比較這兩個例子。我們看到了,在Javascript里面我們所有的設想 都變成了現實。(1)首先關于能夠透傳一切的超級結構,Javascript中實現了閉包的機制,保證了在這種內部的函數對象可以訪問到定義它的環境能訪 問到的所有數據,也就是在例7中的匿名回調函數可以訪問到get_data_js中能訪問到的所有數據。當然,這里重要的是局部數據,如a和b。如果是全 局數據的話左旋肉堿真的有用嗎并不需要通過閉包也能訪問到。而且這個過程是Javascript的運行環境提供的,對于程序員是透明的,程序員并不需要指定哪些參數需要透 傳。(2)不需要再為回調函數命名,因為Javascript支持匿名函數的定義,可以像定義變量一樣定義函數。而這個最終導致了我們在使用異步通信機制 的時候和使用同步的通信機制及其接近,沒有多余的名字,沒有不必要的可見性。