深刻理解回调函数(callback)概念及应用
2020-03-01 17:46:21   来源:   评论:0 点击:

回调函数(callback)是JavaScript(JS)语言的核心,其与异步执行是密切相关的,是必须理解及会应用的一个概念。在理解回调函数(callback...
回调函数(callback)是JavaScript(JS)语言的核心,其与异步执行是密切相关的,是必须理解及会应用的一个概念。在理解回调函数(callback)概念之前,我们先来看生活中的一个场景:

场景:约会结束后你送,你女朋友回家,在女朋友家门口离别时,女朋友贴心地说:“到家了给我发条信息,让我知道你也回到了。”然后你回家以后还真给你女朋友发了条信息。

解析:其实这就是一个回调的过程。女朋友留了个函数b(要求你给女朋友发条信息),然后是你回家的过程,这个回家的过程是函数a。也就是你必须先回到家以后(函数a的内容执行完了),再发送信息给女朋友(执行函数b),然后女朋友就收到一条信息了(回调结果)。

回调函数:回头调用的意思。即函数a有一个参数,这个参数是个函数b,当函数a执行完以后执行函数b,这个过程就叫回调。函数a的事先干完,回头再调用函数b。显然回调函数(callback)有两个重点:

1. 函数可以作为一个参数在另一个函数中被调用。

2. JS是异步编程语言。这就是说,JS代码的执行顺序并不是从上至下按部就班完成的。大多数语言都是同步编程语言,比如现在我们有3行代 码,那么系统一定是一行一行按顺序向下执行的,第一行执行完了,执行第二行,紧跟着最后执行第三行,你可能会说这不是废话吗?且慢 ,在JS里则不尽然,比如有3行代码,并不是排在最前面的代码就是最先执行完毕的,很有可能是最后一行语句最先执行完,然后排在最前 面的那行反而是最后执行完毕的,所以我们说JS是异步编程语言。

应用:我们以nodejs为例,来说明回调函数这个异步过程及正确的使用。

第一步,JS理解异步执行过程:
var fs = require("fs");
var c;

function f(x) {
console.log(x)
}

function writeFile() {
fs.writeFile('input.txt', 'This is the content writed by fs.writeFile', function (err) {
if (!err) {
console.log("文件写入完毕!");
c = 1 ;
}
});
}

c = 0;
writeFile();
f(c)

上述代码就是设置一个全局变量c = 0,然后执行writeFile函数(也就是写入一个文件input.txt),这个函数里面有一行c = 1, 函数执行完毕之后,执行f()函数;其中f()函数很简单,就是把打印一个变量,仅此而已。
按照 “正常” 逻辑,首先c=0,然后调用writeFile函数,该函数里面有一句c = 1,最后再调用f(c);因为调用writeFile()是在f(c)之前, 所以c=1这条语句肯定是会被执行到,那么结果应该是打印1,但是万万想不到,结果竟然是0,明明我们在writeFile函数里我们重新对c进 行了赋值,为什么结果还是0呢? 因为程序运行到writeFile()这一行的时候,是一个比较耗时的IO操作,JS碰到这种操作并不会停在原地一直等待直到函数执行完毕,而是直接运行下一条代码(即f(c)),而此时 c = 1这一行代码其实并没有被执行到,所以打印出来的结果还是0 ! 

那你肯定会说,要解决这个问题还不容易,我们把调用f(c)也放进writeFile函数里面不就行了呗!这样就能保证c = 1之后再调用f(c)了吧? 没错,就这么简单,我们改造一下程序代码:
var fs = require("fs");
var c;

function f(x) {
console.log(x)
}

function writeFile() {
fs.writeFile('input.txt', 'This is the content writed by fs.writeFile', function (err) {
if (!err) {
console.log("文件写入完毕!");
c = 1 ;
f(c);
}
});
}

c = 0;
writeFile();

改造后的这个代码逻辑非常清晰,就是把f(c)放进了writeFile()里面,那么c=1必然会被执行到,然后才执行f(c),不用 多说,结果肯定是显示为1。但是改成这样并不完美,因为这么做就相当于将f()"焊死"在writeFile()里了,如果此处我最终想调用的函数不是 f()而是别的其他函数咋整?难不成要写几个不同的writeFile(),而他们之间的区别仅仅是最后调用的那个函数不同?这样也太笨了吧,于是今天的主角:关键字callback登场了。

第二步,应用回调函数(callback)
我们再改造一次带有回调函数(callback)的代码:
var fs = require("fs");
var c;

function f(x) {
console.log(x)
}

function writeFile(callback) { //使用了关键字callback,也可以是cb,表示这个参数不是一个普通变量,而是一个函数
fs.writeFile('input.txt', 'This is the content writed by fs.writeFile', function (err) {
if (!err) {
console.log("文件写入完毕!");
c = 1 ;
callback(c); //因为我们传进来的是函数名~f,所以相当于调用了一次f(c)
}
});
}

c = 0;
writeFile(f);

经过改造后的代码出现了两次callback关键字,第一个callback出现在writeFile的形参里,起定义的作用,表示这个参数并不是一个普通变 量,而是一个函数,也就是前面所说的重点1,即所谓的“以函数为参数”。 第二个callback出现在c = 1下面,表示此处“执行”从形参传 递进来的那个函数。这样一来,writeFile()函数在执行完毕之后到底调用哪个函数就变“活”了,如果我们想writeFile()函数执行完之后并 不是像第二个例子那样只能调用f(),而是还有别的函数比如说x() y() z(),那么只需要写成 writeFile(x),writeFile(y)... 就行了。

相信你已经看明白上面的代码,因为实在并不高深,那么我们现在开始用一句话攻略做一个总结: 在大多数编程语言中,函数的形参总是由外往内向函数体传递参数,但在JS里如果形参是关键字"callback"则完全相反,它表示函数体在完 成某种操作后由内向外调用某个外部函数。

第三步,熟悉回调函数的应用
有时候,我们会看到一些函数的形参列表里又出现一个函数定义的情况,初时感觉一头雾水,其实只要你了解了上面的内容,看这种直接在 函数调用的时候嵌入一个function的写法会很简单,其本质上仍然是回调函数,因为没有了函数名,所以也称匿名函数。这种情形在JS中更加常见。如本例如果要写成这种风格的话就是长成这样了:
var fs = require("fs");
function writeFile(callback) {
fs.writeFile('input.txt', '我是通过fs.writeFile 写入文件的内容', function (err) { if (!err) {
console.log("文件写入完毕!");
c = 1;
callback(c);
}
});
}

var c = 0;
writeFile(function (x) {
console.log(x);
})

上述代码中,writeFile()函数不变,只是在调用它的时候,直接将函数体嵌在实参里,其作用跟上一个例子完全一样。实际上,本例中fs.writeFile函数后面也跟了一个匿名回调函数 function (err) {},这个函数表示当文件写入完毕后,就回调它,如果在写入过程中出现了错误,则通过变量err 携带出来。相信有了前面的铺垫,大家肯定能理解它的含义了,事实上这种写法在JS里是最常见的主流风格。

第四步:应用示例
//callback.js
function a(callback){
console.log("I am F(a),first exec");
console.log("while finish, exec callback()");
callback();
}

function b() {
console.log("I am callback F(b)");
}

function c(){
console.log("I am callback F(c)");
}

function test(){
a(b);
a(c);
}

test();

console.log("another callback form: a(function(){...})");

a(function(){
console.log("I am a anonynmous callback F()");
})

可能通过node callback.js来查看示例执行结果:
I am F(a),first exec
while finish, exec callback()
I am callback F(b)
I am F(a),first exec
while finish, exec callback()
I am callback F(c)
another callback form: a(function(){...})
I am F(a),first exec
while finish, exec callback()
I am a anonynmous callback F()

【补充】在JS里,并非所有操作都是异步的,比如for循环(无论这个for循环需要耗时多长,系统也一定会等它转完之后才会执行下面的语句,这一点跟其他大部分同步语言是一致的)。JS中会产生异步执行的操作大概有以下几种: 定时器、建立网络连接、读取网络流数据、向文件写入数据、Ajax提交、请求数据库服务,等等。

相关热词搜索:回调函数 callback

上一篇:linux常用的操作技巧命令
下一篇:最后一页

分享到: 收藏