Javascript的回调机制的经典教程
由于其运行环境的特殊性,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);