前言
callback,大家都知道是回调函数的意思。但是你对这个概念应该是模模糊糊。比如Ajax,你只知道去调用返回函数,如果对callback没有理解清楚,估计你在学习Node.js后会崩溃,因为callback是Node.js三大核心之一。
一 .回调函数
回调函数的概念
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
字面上理解下来就是,回调就是一个函数的调用过程。那么就从理解这个调用过程开始吧。函数a有一个参数,这个参数是个函数b,当函数a执行完以后执行函数b。那么这个过程就叫回调。
其实中文也很好理解:回调,回调,就是回头调用的意思。函数a的事先干完,回头再调用函数b。
下面给一个回调的例子
function a(callback) { alert("我是parent函数a!"); alert("调用回调函数"); callback(); } function b(){ alert("我是回调函数b"); } function c(){ alert("我是回调函数c"); } function test() { a(b); a(c); }复制代码
从上面的例子,我们可以看出回调与同步、异步并没有直接的联系,回调只是一种实现方式,既可以有同步回调,也可以有异步回调,还可以有事件处理回调和延迟函数回调,这些在我们工作中有很多的使用场景。
二.为什么写回调函数
1.关于回调函数和js单线程以及js异步机制
我们都知道js是单线程的,这种设计模式给我们带来了很多的方便之处,我们不需要考虑各个线程之间的通信,也不需要写很多烧脑的代码,也就是说js的引擎只能一件一件事的去完成和执行相关的操作,所以所有需要执行的事情都像排队一样,等待着被触发和执行,可是如果这样的话,如果在队列中有一件事情需要花费很多的时间,那么后面的任务都将处于一种等待状态,有时甚至会出现浏览器假死现象,例如其中有一件正在执行的一个任务是一个死循环,那么会导致后续其他的任务无法正常执行,所以js在同步机制的缺陷下设计出了异步模式
在异步执行的模式下,每一个异步的任务都有其自己一个或着多个回调函数,这样当前在执行的异步任务执行完之后,不会马上执行事件队列中的下一项任务,而是执行它的回调函数,而下一项任务也不会等当前这个回调函数执行完,因为它也不能确定当前的回调合适执行完毕,只要引它被触发就会执行,举个例子,
图片资源还未请求完毕
上图可以看到,我要购买一个东西,当我点进物品的详情页之后,图片资源还未请求完毕,而此时我就可以点击add to cart, 发起另一个请求,js任务列表中的添加购物车事件就会开始执行,它的执行也不会干扰到图片资源的请求任务,这也是异步执行机制的妙处
2.js的单线程浏览器内核的多线程
说到js的单线程,顺便再了解一下关于浏览器内核的多线程,关于浏览器工作原理此处不做讲解,因为自己研究的不深入,等待研究学习研究透彻再分享
cayley的草图
浏览器常驻三大线程: js引擎线程,GUI渲染线程,浏览器事件触发线程
看到此图你是不是会豁然开朗许多,因为浏览器是一个多线程的执行环境,在浏览器的内核中分配了多个线程,最主要的线程之一即是js引擎的线程,同时js事件队列中的异步请求,交互事件触发,定时器等事件都是由浏览器的事件触发线程进行监听的,浏览器的事件触发线程被触发后会把任务加入到js 引擎的任务队列中,当js 引擎空闲时候就会开始执行该任务
三.回调函数中的this指向问题
使用js中的定时器(setInterval,setTimeout),很容易会遇到this指向的问题。
直接上例子:
1 var name = 'my name is window'; 2 var obj = { 3 name: 'my name is obj', 4 fn: function () { 5 var timer = null; 6 clearInterval(timer); 7 timer = setInterval(function () { 8 console.log(this.name); //my name is window 9 }, 1000) 10 } 11 }复制代码
在这里,从this.name可以看出this的指向是window。
如果没有特殊指向,setInterval和setTimeout的回调函数中this的指向都是window。这是因为JS的定时器方法是定义在window下的。但是平时很多场景下,都需要修改this的指向。这里总结了几种:
1、最常用的方法:在外部函数中将this存为一个变量,回调函数中使用该变量,而不是直接使用this。
1 var name = 'my name is window'; 2 var obj = { 3 name: 'my name is obj', 4 fn: function () { 5 var that = this; 6 var timer = null; 7 clearInterval(timer); 8 timer = setInterval(function () { 9 console.log(that.name); //my name is obj 10 }, 1000) 11 } 12 }复制代码
在fn中加了var that = this; 回调函数中使用that代替this即可。这种方法最常见,使用也最广泛。
2、使用bind()方法(bind()为ES5的标准,低版本IE下有兼容问题,可以引入es5-shim.js解决)
bind()的作用类似call和apply,都是修改this指向。但是call和apply是修改this指向后函数会立即执行,而bind则是返回一个新的函数,它会创建一个与原来函数主体相同的新函数,新函数中的this指向传入的对象。
1 var name = 'my name is window'; 2 var obj = { 3 name: 'my name is obj', 4 fn: function () { 5 var timer = null; 6 clearInterval(timer); 7 timer = setInterval(function () { 8 console.log(this.name); //my name is obj 9 }.bind(this), 1000) 10 } 11 }复制代码
在这里为什么不能用call和apply,是因为call和apply不是返回函数,而是立即执行函数,那么,就失去了定时器的作用。
3、使用es6的箭头函数:箭头函数的最大作用就是this指向。
1 var name = 'my name is window'; 2 var obj = { 3 name: 'my name is obj', 4 fn: function () { 5 var timer = null; 6 clearInterval(timer); 7 timer = setInterval(() => { 8 console.log(this.name); //my name is obj 9 }, 1000) 10 } 11 }复制代码
箭头函数没有自己的this,它的this继承自外部函数的作用域。所以,在该例中,定时器回调函数中的this,是继承了fn的this。当然箭头函数也有兼容问题,要是兼容低版本ie,需要使用babel编译,并且引入es5-shim.js才可以。
四.回调函数是实现异步编程的利器
在程序运行中,当某些请求过程漫长,我们有时没必要选择等待请求完成继续处理下一个任务,这时使用回调函数进行异步处理可以大大提高程序执行效率。例如:AJAX请求。若是使用回调函数进行处理,代码就可以继续进行其他任务,而无需空等。实际开发中,经常在javascript中使用异步调用!
下面有个使用AJAX加载XML文件的示例,并且使用了call()函数,在请求对象(requested object)上下文中调用回调函数。
function fn(url, callback){ var httpRequest; //创建XHR httpRequest = window.XMLHttpRequest ? new XMLHttpRequest() : //针对IE进行功能性检测 window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : undefined; httpRequest.onreadystatechange = function(){ if(httpRequest.readystate === 4 && httpRequest.status === 200){ //状态判断 callback.call(httpRequest.responseXML); } }; httpRequest.open("GET", url); httpRequest.send(); } fn("text.xml", function(){ //调用函数 console.log(this); / /此语句后输出 }); console.log("this will run before the above callback."); //此语句先输出复制代码
我们请求异步处理,意味着我们开始请求时,就告诉它们完成之时调用我们的函数。在实际情况中,onreadystatechange事件处理程序还得考虑请求失败的情况,这里我们是假设xml文件存在并且能被浏览器成功加载。这个例子中,异步函数分配给了onreadystatechange事件,因此不会立刻执行。
最终,第二个console.log语句先执行,因为回调函数直到请求完成才执行。
在Javascript编程中回调函数经常以几种方式被使用,尤其是在现代web应用开发以及库和框架中:
- 异步调用(例如读取文件,进行HTTP请求,动态加载js文件,加载iframe资源后,图片加载完成执行回调等等)
- 事件监听器/处理器
- setTimeout和setInterval方法
- 一般情况:精简代码
五.“回调地狱”问题以及解决方案
这么多回调嵌套,我还没遇到过。下面内容是直接从网上copy过来,大家看下,还是很好理解的。
在执行异步代码时,无论以什么顺序简单的执行代码,经常情况会变成许多层级的回调函数堆积以致代码变成下面的情形。这些杂乱无章的代码叫做回调地狱因为回调太多而使看懂代码变得非常困难。我从node-mongodb-native,一个适用于Node.js的MongoDB驱动中拿来了一个例子。这段位于下方的代码将会充分说明回调地狱:
var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory}); p_client.open(function(err, p_client) { p_client.dropDatabase(function(err, done) { p_client.createCollection('test_custom_key', function(err, collection) { collection.insert({'a':1}, function(err, docs) { collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) { cursor.toArray(function(err, items) { test.assertEquals(1, items.length); // Let's close the db p_client.close(); }); }); }); }); }); });复制代码
你应该不想在你的代码中遇到这样的问题,当你当你遇到了-你将会是不是的遇到这种情况-这里有关于这个问题的两种解决方案。
给你的函数命名并传递它们的名字作为回调函数,而不是主函数的参数中定义匿名函数。
模块化L将你的代码分隔到模块中,这样你就可以到处一块代码来完成特定的工作。然后你可以在你的巨型应用中导入模块