详解回调函数
什么是回调函数? 下面这个是吗
function func(para, cb) { console.log(para); var num = Math.random(); cb(num); } var callback = function (num) { console.log(num); } func(1, callback);
这里,我定义了一个函数,函数接收一个函数作为参数,然后再函数内部调用这个接收的函数。 另外,我定义了传入的函数为callBack,这就是回调了吗? no,no,no!
下面是我们经常容易混淆的地方:
- 定义了一个匿名函数callBack,然后它就是回调函数了? 否
- func的第二个参数cb,即callback的简称,他就是回调了? 否
- 只要callback被作为参数,传递到函数中被调用,它就是回调了? 否
上面的callback函数的调用,实际上就是普通的函数调用,跟回调没有一点关系!
在《JavaScript设计模式》还是《你不知道的JavaScript》中对闭包的讲解时设计到过这些问题。
个人理解:
同步、异步: 是否有主动通知的功能? 如果会主动通知,即为异步;否则为同步。
阻塞、非阻塞: 是否在一直等待? 如果是,即为阻塞; 否则为非阻塞。
回调:英文名为callback,即打电话过来之意。你去一个商店买东西,暂时没有货; 你留下电话, 并委托一件事情---有货了给我打电话。 有货了它就打来电话,然后你去取货。
在这个过程中,你的电话号码就是回调函数。你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。具体这么多也不重要,重要的是callback才是回调函数,即不是你主动的去调用函数,而是系统主动的调用你写的函数。
然后我们再来看之前写的那个函数中的这两条语句:
var num = Math.random(); cb(num);
可以发现,这里我们已经得到了一个随机数,所以cb(num);的时候就直接调用了这个函数,所以也就不存在是系统来调用你的函数了。故你还是主动方,是你调用了函数。除非它给你打了电话(callback),否则是不能称为回调函数的。
以nodejs中的readFile这个api进一步说明:
fs.readFile(filename, [options], callback)
有两个必填的参数 filename 和 callback
callback是实际程序员要写代码的地方,写它的时候假设文件已经读取到了,该怎么写还怎么写,是API历史上的一次大进步。
这里的callback才是回调,为什么呢?
这是因为我们不知道什么时候filename才能被读取结束,但是我们告诉了它等到你读取结束了就要给我说一声,帮我执行callback。代码如下:
//读取文件'etc/passwd',读取完成后将返回值,传入function(err, data) 这个回调函数。 fs.readFile('/etc/passwd', function (err, data) { if (err) throw err; console.log(data); });
这段代码对于人们的疑惑常常是,我怎么知道callback要接收几个参数,参数的类型是什么?
答:是API提供者事先设计好的,它需要在文档中说明callback接收什么参数。
那么EventLoop是什么呢?我们看下面的例子
function Add(a, b){ return a+b; } function LazyAdd(a, cb){ return function(b){ cb(a, b); } } var result = LazyAdd(1, Add) // 假设有一个变量button为false,我们继续调用result的条件是,当button为true的时候。 var button = false; // 常用的办法是观察者模式,派一个人不断的看button的值, //只要变了就开始执行result(2), 当然得有别人去改变button的值, //这里假设有人有这个能力,比如起了另外一个线程去做。 while(true){ if(button){ result = result(2); break; } } result = result(2); // => 3
这个实际上就是异步了,cpu开启一个线程来看着什么时候满足条件,一旦满足条件就告诉我们然后执行,而在主线程上还是可以继续做其他的事情。
所以说回调的好处就是可以实现异步,但是这里明显太麻烦了,如果每次一个异步都要这样,一定效率会有下降。
于是,这时EventLoop诞生了,派一个人来轮询所有的,其他人都可以把观察条件和回调函数注册在EventLoop上,它进行统一的轮询,注册的人越多,轮询一圈的时间越长。但是简化了编程,不用每个人都写轮询了,提供API变得方便,就像fs.readFile一样简单明白,fs.readFile读取文件’/etc/passwd’,将其注册到EventLoop上,当文件读取完毕的时候,EventLoop通过轮询感知到它,并调用readFile注册时带的回调函数,这里是funtion(err, data)。
换一个说法再说一遍:在特定条件下,单台机器上用空间换计算。原本的时候callback执行了就不等了,存在一个地方,其他依赖它的,用观察着模式一直盯着它,各自轮询各自的。现在有人出来替大家统一轮询。那么显然这样就可以提高效率。
总之,整个过程就是异步->回调->EventLoop
但是callback也是会有问题的,比如下面的回调地狱:
fs.readFile('/etc/password', function(err, data){ // do something fs.readFile('xxxx', function(err, data){ //do something fs.readFile('xxxxx', function(err, data){ // do something }) }) })
即嵌套十分严重,在es6中给出了generator, 后续会讲到。