大家知道javascript中有多少方法能够实现异步处理吗?setTimeout(),setInterval()是最常用的两个。XMLHttpRequest对象,进行ajax请求时。postMessage()进行跨域操作时。WebWorker创建新的线程时。setImmediate方法(新的setTimeout方法)。requestAnimationFrame进行动画操作时。这些东西都有一个共同的特点,就是拥有一个回调函数。有的异步API还提供了相对应的中断API,比如:clearTimeout,clearInterval,clearImmediate,cancelAnimationFrame。
早些年,我们就是通过setTimeout和setInterval在网页上实现动画的,这种动画其实就是通过异步API不断的调用同一个回调方法实现的,回调方法里面对元素节点的某些样式进行很小范围的改动。
首先,我们来讲一下setTimeout和setInterval这两个API,这里只讲它不常见的知识点:
(1)它们的回调方法,如果执行时间大于间隔时间(比如:setInterval(function(){里面执行代码的事件大于50毫秒},50)),那么实际上的间隔时间会大于50毫秒(因为js执行线程其实是一个队列,它执行完一个函数后,才会执行另外一个函数,因此,这段代码的意思是:每隔50毫秒,给执行线程添加一个函数,如果执行线程在执行这个函数时,后面又来了一个函数,后面这个函数会在那里排队,要等前面那个函数执行完成,它才会执行,因此实际上函数执行的间隔时间大于50毫秒)。
(2)它们存在一个最小的时钟间隔,IE6-8下为15.6ms,IE9为10ms,IE10和其他标准浏览器为4ms。意思就是说,如果你setInterval(functiojn(){},1),这个代码的意思是一毫秒就执行一次function,但是实际上,浏览器它们有一个最小的间隔,即便你写了1ms,它也会按照它的这个最小事件间隔来执行function(比如:IE6-8会15.6毫秒才执行一次function)。如果你觉得IE6-8下,最短时钟间隔太大,你可以利用image死链时立即执行onerror回调的情况进行改造,比如:
var orig_setTimeout = window.setTimeout;
window.setTimeout = function(callback,time){
if(time > 15) {
orig_setTimeout(callback,time);
}
else{ //当间隔时间小于15毫秒时,就新建一个Image对象,给它一个错误的src,这时会立即调用onerror回调方法,这时就会执行callback,实现间隔时间小于15毫秒的功能。
var img = new Image();
img.onload = img.onerror = function(){
callback();
};
img.src = "data:,foo";
}
}
(3)不写第二个参数时,浏览器自动分配时间,IE,Firefox中,第一个分配可能给个100ms,往后会慢慢缩小到最小时钟间隔。Safari,Chrome,Opera则分配一个10ms。Firefox中,setInterval不写第二个参数,会当做setTimeout处理,只执行一次。
(4)IE10+和标准浏览器支持额外参数,从第三个参数起,作为回调的传参传入。比如:setTimeout(function(){},1000,1,2,4),那么function中的[].slice.call(arguments) = [1,2,4]。IE6-9可以这样模拟:
if(IE9-){
(function(overrideFun){
window.setTimeout = overrideFun(window.setTimeout);
window.setInterval = overrideFun(window.setInterval);
})
(
function(originalFun){
return function(callback, delay){
var args = [].slice.call(arguments,2); //从第三个参数开始取
return originalFun(function(){
if(typeof callback == "string"){ //如果第一次参数传入的是字符串
eval(callback);
}else{
callback.apply(this,args); //把参数传入回调方法
}
},delay);
};
}
)
}
(5)setTimeout方法的事件参数若为负数或0或极大的正数,标准浏览器都立即执行,而老版本的IE处理会出现较大的差异,不用研究。
接下来,我们来讲解下Deferred对象(jQuery中的ajax异步处理对象)的前身Mochikit Deferred。
Deferred是当今最著名的异步模型,它原来是Python的Twisted框架的一个类,后来被Mochikit框架引进来,后面被dojo抄去,jQuery后面也引进。我们来详细讲一下Mochikit Deferred的实现原理(接下来的Deferred指的是Mochikit Deferred):
Deferred内部把回调分成两种,一种是成功回调,用于正常时执行,一种叫错误回调,用于出错时执行。各自组成两个队列,我们可以叫做成功队列与错误队列。在添加回调时,它是一组一组(成功回调和错误回调)的添加的,每组的回调只会执行一个(不是执行成功回调,就是执行错误回调),每组回调接收到的参数,都是上一组回调处理后返回的结果,只有第一次组的回调接收的参数是用户传入的。那么如何决定是执行成功回调还是错误回调呢,也是根据上一组的结果决定的,如果上一组的结果抛出错误,那么这一组就会执行错误回调,如果这一组的错误回调不抛出错误,那么下一组的回调就执行成功回调。第一组的执行是用户决定的,意思就是用户调用成功回调的方法,就执行成功回调,用户执行错误回调的方法就执行错误回调。
我们先来看一下Deferred里面的方法:
addCallback 添加成功回调的方法。
addErrback 添加错误回调的方法
addBoth 同时添加正常回调和错误回调的方法
这三个方法内部都会调用addCallbacks方法,而这个方法的参数只能是两个函数或一个函数一个null。也就是说上面的三个方法会把参数转换成两个函数或一个函数一个null,然后传给addCallbacks方法。Deferred实例有一个chain数组属性,数组中的每一项都是一个双元素的数组,比如:
deferred.chain = [ [callback,errorcallback], [callback1,errorcallback1], [callback2,errorcallback2] ];
举个例子:
var d = new Deferred();
d.addCallback(myCallback);
d.addErrback(myErrback);
d.addBoth(myBoth);
d.addCallbacks(myCallback,myErrback);
这时,d.chain = [ [myCallback, null], [null, myErrback], [myBoth, myBoth], [myCallback, myErrback] ];
触发这些回调是通过调用d.callback和d.errback方法实现的。这两个方法里面的流程是一致的,首先检查此Deferred对象有没有被调用过,如果没有,就调用_resback方法。
当然用户可以在callback或errback中传入参数,传入的参数在_resback方法中会生成一个数组,如果调用的是callback方法(成功的回调),那么就把参数放到数组的第一个位置,如果调用的是errback方法(失败的回调),那么就把参数放到数组的第二个位置。然后_resback方法会判断执行有没有被切断(异步过程有没有被终止),没有的话,就调用_fire方法执行回调。
_fire方法就是不断弹出chain数组中的一组函数,根据状态取第一个回调还是第二个回调(第一个是成功回调,第二个是失败回调)执行,每一组回调都接收上一组回调的返回值作为参数。举个例子:
function increment(value){
console.log(value);
return value+1;
}
var d = new Deferred();
d.addCallback(increment); //d.chain = [[increment,null]]
d.addCallback(increment); //d.chain = [[increment,null],[increment,null]]
d.addCallback(increment); //d.chain = [[increment,null],[increment,null],[increment,null]]
d.callback(1); //_resback(1)-> [1,null] -> fire([1,null]) -> 循环取出d.chain中的每一组函数, 因为传入的数组第一项是1,就代表成功回调,因此执行第一组函数的成功回调increment方法,这时打印出1,返回2,而这个2会继续传给第二组函数,因为也是成功回调,所以就执行第二组函数的成功回调increment方法,打印出2,返回3。以此类推。
那么失败回调什么时候执行呢,在以上的执行流程中,会有一个try catch,如果回调方法抛出错误,就会catch住,然后执行下一组函数中的失败回调。举个例子:
var d = new Deferred();
d.addCallback(function(a){ console.log(a);return 4}).addBoth(function(a){console.log(a);throw "抛错"},function(b){console.log(b);return "xx"}).addBoth(function(a){console.log(a); return "正常"},function(b){console.log(b);return "出错"}).addBoth(function(a){console.log(a + "正常")},function(b){console.log(b+"继续出错")})
d.callback(3);
因为调用的是callback方法所以是成功回调,因此打印出a(也就是3),返回4给下一组函数,下一组函数收到这个4,因为没有抛出错误,所以是成功回调,因此打印出4,throw "抛错",这时下一组函数,就会执行失败回调,也就是function(b){console.log(b);return "出错"},打印出Error:抛错,返回"出错",这时没有抛出错误,而是正常返回"出错"这个字符串,因此下一组函数,就会执行成功回调,也就是function(a){console.log(a + "正常")},这时打印出"出错正常"。
在Mochikit中,Deferred还有一个重要的功能就是,可以并归多个Ajax的请求结果,然后再做处理。它是使用DeferredList实现的。早期的jQuery和Prototype没有这个东西,它们之前是使用计数器实现的,非常复杂。举个例子:
有一个业务,需要发起4个Ajax请求,这4个请求的地址和返回时间不一样,必须等到它们都处理完成后,整合它们4个的返回数据,然后根据这个整合的数据再发起两个ajax请求,等到这两个ajax请求全部处理完成,整合这两个请求的数据后,再进行一次ajax请求,返回数据才算一次业务处理成功。如果你不使用异步对象来处理,你可以想想你的代码会写的多复杂,而且多容易出错。如果使用DeferredList来实现,非常简单,先把那4个ajax请求放到DefrredList对象d1中,然后4个ajax全部处理完成后,才会触发DefrredList对象的回调,而在这个回调中把这4个请求的数据整合,然后根据整合的数据发起两个ajax请求,这两个ajax请求又放到一个DefrredList对象d2中,等这两个ajax请求全部处理完成后,就会执行d2的回调方法,最后在这个回调中整合这2个请求的数据,发送最后一次ajax请求,就OK了。
下一课,将讲解JSDeferred对象,它基本奠定了后来称为Promise/A的范式。
加油!