本文深入解析 JavaScript 回调函数的核心概念,阐述其作为设计模式在同步与异步场景中的应用。通过代码示例说明回调在 AJAX、DOM 事件及 Node.js 中的实现,并探讨链式调用与定时器回调的最佳实践,帮助开发者避免轮询低效问题,提升代码执行效率。
回调函数的基本概念
将函数作为参数传递给另一个函数,该被传递的函数即被称为回调函数。
在实际项目开发中,常出现 A 层与 B 层由不同开发人员协作的情况。A 层负责实现功能函数 funA,B 层负责 funcB。当 B 层需要调用 A 层的模块数据时,B 层人员会向 A 层人员提出需求,要求提供满足特定条件数据的接口。
A 层人员提供数据,而数据的展示与后续处理则由 B 层自行决定。由于 B 层不可能为每个具体需求都定制一个数据接口,因此 B 层会提供一个通用的接口规范。B 层获取数据后,编写相应的函数来进行展示和处理。
这种协作模式意味着你需要与他人合作,由他人提供数据,而无需关注数据的获取或构建方式。你只需对获取到的数据进行操作,此时便需要使用回调函数。
因此,回调本质上是一种设计模式,jQuery 及其他主流框架的设计原则均遵循了这一模式。
以下是一个在同步(阻塞)场景中使用回调的例子,旨在确保 func1 代码执行完毕后立即执行 func2。
代码如下:
var func1=function(callback){
//do something.
(callback && typeof(callback) === "function") && callback();
}
func1(func2);
var func2=function(){
}
同步与异步回调示例
以下是异步回调的示例:
代码如下:
$(document).ready(callback);
$.ajax({
url: "test.html",
context: document.body
}).done(function() {
$(this).addClass("done");
}).fail(function() {
alert("error");
}).always(function() {
alert("complete");
});
需要注意的是,AJAX 请求确实是异步的,但该请求由浏览器新开一个线程发起。当请求状态发生变化时,如果此前已设置回调,该异步线程会产生状态变更事件并将其放入 JavaScript 引擎的处理队列中等待处理。详见:http://www.phpv.net/html/1700.html
回调的执行时机与场景
回调函数的执行时机
在同步情境下,回调函数通常在最后执行;而在异步情境下,回调函数可能不会执行,因为触发事件的条件可能未满足或事件未被触发。
回调函数的使用场景
资源加载:动态加载 JS 文件后执行回调,加载 iframe 后执行回调,AJAX 操作回调,图片加载完成执行回调等。DOM 事件及 Node.js 事件均基于回调机制(Node.js 回调可能会出现多层回调嵌套的问题)。setTimeout 的延迟时间设为 0 是一种常用技巧(hack),此时 setTimeout 调用的函数实质上就是回调函数的体现。
链式调用:在赋值器(setter)方法中(或本身没有返回值的方法中)很容易实现链式调用。取值器(getter)相对难以实现,因为需要返回所需数据而非 this 指针。若要实现链式方法,可通过回调函数来实现。
获取 setTimeout、setInterval 的函数返回值。由于这两个函数都是异步的,其调用时序与程序主流程相对独立,因此无法在主流程中等待它们的返回值。函数启动时程序不会暂停等待,否则便失去了 setTimeout 及 setInterval 的意义。因此,使用 return 已无意义,只能使用 callback。callback 的作用在于将 timer 执行的结果通知给代理函数以便及时处理。
综合示例与最佳实践
网上收集了一些资料,经过理解后,自己整理出一个示例:
代码如下:
function fun(num,callback){
if(num<0) {
alert("调用A层函数处理!");
alert("数据不能为负,输入错误!");
}else if(num==0){
alert("调用A层函数处理!");
alert("该数据项不存在!");
}else{
alert("调用B层函数处理!");
callback(1);
}
}
function test(){
var num=document.getElementById("score").value;
fun(num,function(back){ //匿名B层处理函数
alert(":"+back);
if(num<2)
alert("数字为1");
else if(num<=3)
alert("数字为2或3!");
else
alert("数字大于3!");
})
}
当函数开始执行 fun 的时候,先判断 num 是否是负数或者为零,否则执行 B 层处理函数 alert(":"+back) 输出 1,并判定为 <2、<=3、>3 等情况。
经验小提示:
最好保证回调存在且必须是函数引用或者函数表达式:
代码如下:
(callback && typeof(callback) === "function") && callback();
代码如下:
var obj={
init : function(callback){
//TODO ...
if(callback && typeof(callback) === "function") && callback(){
callback('init...');//回调
}
}回调与轮询的性能对比
最后,关于为什么要使用回调函数呢?下面的比喻很生动有趣。
你有事去隔壁寝室找同学,发现人不在,你怎么办呢?
方法 1,每隔几分钟再去趟隔壁寝室,看人在不 方法 2,拜托与他同寝室的人,看到他回来时叫一下你
前者是轮询,后者是回调。
那你说,我直接在隔壁寝室等到同学回来可以吗?
可以啊,只不过这样原本你可以省下时间做其他事,现在必须浪费在等待上了。把原来的非阻塞的异步调用变成了阻塞的同步调用。
JavaScript 的回调是在异步调用场景下使用的,使用回调的性能优于轮询。