关于callback函数我所知道的一切

距上一篇博文已经好长时间了,这几个月入职公司实习,忙毕设设计,出去找房……大四狗的生活好累啊(抽泣脸)。回到博文本身,因为一直对使用 Node.js 写一些后台程序充满了极大的兴趣,不过直到入职公司的时候才开始逐步入门学习 Node.js 开发知识。学习 Node.js 最主要的学会如何异步编程,异步编程使用了 callback 函数,于是决定来认真学习一下 callback 函数的概念以及用法

扯扯 Node.js

最近学习 Node.js,看了一本挺不错的 Node.js 基础入门书《Node.js Succinctly》。了解到 Node.js 赖以成名的异步非阻塞特性。那何为异步非阻塞呢?首先我们得明白 JavaScript 本身就是一门事件驱动(观察者模式)和单线程的语言,它的特性是跟 JavaScript 的诞生背景是相关的。JavaScript 一开始设计出来就是为了丰富 Web 的交互效果也就是DOM操作,进行简单的表单验证。同样,Node.js 的执行程序本就是单线程,因为同样也是用 JavaScript(当初设计 Node.js 时决定使用 JavaScript 也是看重它单线程语言的特点)。单个线程如果遇到耗时操作例如 I/O操作,网络访问时则会阻塞该线程后面的执行流程,因此为了不阻塞这唯一的执行线程,因此就利用了异步的方式。所谓异步的方式就是让耗时操作在其他线程中完成的,我们姑且称他们为工作线程。当某些耗时操作完成后就需要将执行的结果返回给唯一的主线程,这一步则是通过 callback 函数完成的,同时也体现了事件驱动的特点。因此,我们可以说异步编程最直接的表现就是回调,但 callback 函数跟异步/同步并没有直接的关系。例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function heavyCompute(n, callback){
let count = 0,
i,
j;
for (i = n; i > 0;--i){
for(j = n; j > 0;--j){
count += 1;
}
}
callback(count);
}

heavyCompute(10000, function(count){
console.log(count);
});

console.log('hello');

执行的结果为:

100000000

hello

可以看到,以上代码的回调函数仍然先于后续代码执行,也可以看出 JavaScript 函数也是一等公民的特点,毕竟也可以作为函数参数。这种方式就叫同步回调,而异步回调就类似于在写页面时经常会遇到的点击按钮触发弹出框弹出的动作,如下:

1
2
3
4
5
6
......
let btn = document.getElementByID('myButton');
btn.addEventListen('click',function(ev){
console.log(ev.target);
});
......

什么是 callback 函数

绕远一点来说,编程分为系统编程和应用编程。系统编程就是编写库,这些编写的库函数相对来说更接近硬件。而应用编程就是利用写好的各种库来编写具有某种功用的程序。系统程序员会给自己编写的库留下一些接口,即 API (application programming interface,应用编程接口),以供应用程序员使用。所以在抽象层的图示里,库位于应用的底下。

当程序跑起来时,一般情况下应用程序会时常通过 API 调用库函数。这时会出现这样一种情况:某些库函数要求应用先传给它一个函数,好在合适的时候调用,已完成目标任务。这个被传入的,后又被调用的函数就称为 callback 函数。

callback 函数调用过程

可以看到,callback 函数通常和应用处于同一抽象层(因为传入什么样的 callback 函数是在应用这一层决定的)。而所谓的“callback”就体现在:高层函数调用底层函数即库函数,底层函数回过头来调用高层函数的过程

如果以饭馆吃饭买单的过程来比喻的话:在客人招呼服务员买单的时候,服务员就根据手上的订单计算总价后再告诉客人应付多少钱。这一过程中,客人招呼服务员买单就像应用程序的高层函数去调用底层函数,这里应用程序指的就是这一桌客人要买单的上下文环境,底层函数就是服务员计算总价的过程。服务员计算完总价之后再告诉客人就类似于底层函数调用高层函数,回到客人买单的上下文环境中去。

为什么需要 Callback 函数

callback 这种机制比简单的函数调用有着更大的灵活性。除了上面那种 callback 机制的应用场景外,其实任何时候我们都可以实现上面的那种 callback 机制,而不仅仅在应用和库之间。因此,从这里开始我们把上面的库函数称为中间函数。那 callback 机制的灵活性是如何实现的呢?我们可以认为中间函数在没有传入 callback 函数之前逻辑上是不完整的。换句话说,程序可以在运行时通过传入不同的 callback 函数来改变中间函数的行为,这比简单的函数调用要灵活得多。我们来看看用 python 写的示例:

even.py

1
2
3
4
5
6
7
8
9
#回调函数1
#生成一个2k形式的偶数
def double(x):
return x * 2

#回调函数2
#生成一个4k形式的偶数
def quadruple(x):
return x * 4

callback_demo.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from even import *

#中间函数
#接受一个生成偶数的函数作为参数
#返回一个奇数
def getOddNumber(k, getEvenNumber):
return 1 + getEvenNumber(k)

#起始函数,这里是程序的主函数
def main():
k = 1
#当需要生成一个2k+1形式的奇数时
i = getOddNumber(k, double)
print(i)
#当需要一个4k+1形式的奇数时
i = getOddNumber(k, quadruple)
print(i)

if __name__ == "__main__":
main()

运行callback_demp.py,输出如下:

3

5

上面的代码里,给getOddNumber传入不同的回调函数,它的表现也不同,这就是 callback 机制的优势所在。

但是在这里我们得强调:通过上面的论述,中间函数和 callback 函数是 callback 机制的两个必要部分,不过人们往往忽略了 callback 机制的起点:起始函数。它也是中间函数的调用者,在大部分情况下,可以把这个起始函数和程序的主函数等同起来。因此 callback 机制是起始函数、中间函数和 callback 函数的三方联动。同时起始函数和 callback 函数也不能简单看为一体。因为 callback 实际上有两种:阻塞式和延迟式。阻塞式,callback 函数的调用一定发生在起始函数返回之前;而延迟式,callback 函数的调用有可能是在起始函数返回之后。因此起始函数和 callback 函数并不为同一事物。

回到 JavaScript 中,为什么我们需要 callback 函数。因此上面我们也说到 JavaScript 是一门事件驱动的语言。这意味着 JavaScript 执行程序不会等待响应,而会在监听其他事件时继续执行。

参考

  1. 回调函数(callback)是什么? - no.body的回答 - 知乎
  2. JavaScript: What the heck is a Callback?|Medium
  3. 七天学会NodeJS
Donate comment here