javascript 异步编程2
好像有这么一句名言——"每一个优雅的接口,背后都有一个龌龊的实现"。最明显的例子,jQuery。之所以弄得这么复杂,因为它本来就是那复杂。虽然有些实现相对简明些,那是它们的兼容程度去不了那个地步。当然,世上总有例外,比如mootools,但暴露到我们眼前的接口,又不知到底是那个父类的东西,结构清晰但不明撩。我之所以说这样的话,因为异步列队真的很复杂,但我会尽可能让API简单易用。无new实例化,不区分实例与类方法,链式,等时髦的东西都用上。下面先奉上源码:
;(function(){ var dom = this.dom = this.dom || { mix : function(target, source ,override) { var i, ride = (override === void 0) || override; for (i in source) { if (ride || !(i in target)) { target[i] = source[i]; } } return target; } } ////////////////////////////////////////////////////////////////////// //=======================异步列队模块=================================== var Deferred = dom.Deferred = function (fn) { return this instanceof Deferred ? this.init(fn) : new Deferred(fn) } var A_slice = Array.prototype.slice; dom.mix(Deferred, { get:function(obj){//确保this为Deferred实例 return obj instanceof Deferred ? obj : new Deferred }, ok : function (r) {//传递器 return r }, ng : function (e) {//传递器 throw e } }); Deferred.prototype = { init:function(fn){//初始化,建立两个列队 this._firing = []; this._fired = []; if(typeof fn === "function") return this.then(fn) return this; }, _add:function(okng,fn){ var obj = { ok:Deferred.ok, ng:Deferred.ng, arr:[] } if(typeof fn === "function") obj[okng] = fn; this._firing.push(obj); return this; }, then:function(fn){//_add的包装方法1,用于添加正向回调 return Deferred.get(this)._add("ok",fn) }, once:function(fn){//_add的包装方法2,用于添加负向回调 return Deferred.get(this)._add("ng",fn) }, wait:function(timeout){ var self = Deferred.get(this); self._firing.push(~~timeout) return self }, _fire:function(okng,args,result){ var type = "ok", obj = this._firing.shift(); if(obj){ this._fired.push(obj);//把执行过的回调函数包,从一个列队倒入另一个列队 var self = this; if(typeof obj === "number"){//如果是延时操作 var timeoutID = setTimeout(function(){ self._fire(okng,self.before(args,result)) },obj) this.onabort = function(){ clearTimeout(timeoutID ); } }else if(obj.arr.length){//如果是并行操作 var i = 0, d; while(d = obj.arr[i++]){ d.fire(args) } }else{//如果是串行操作 try{// result = obj[okng].apply(this,args); }catch(e){ type = "ng"; result = e; } this._fire(type,this.before(args,result)) } }else{//队列执行完毕,还原 (this.after || Deferred.ok)(result); this._firing = this._fired; this._fired = []; } return this; }, fire:function(){//执行正向列队 return this._fire("ok",this.before(arguments)); }, error:function(){//执行负向列队 return this._fire("ng",this.before(arguments)); }, abort:function(){//中止列队 (this.onabort || Deferred.ok)(); return this; }, //每次执行用户回调函数前都执行此函数,返回一个数组 before:function(args,result){ return result ? result instanceof Array ? result : [result] : A_slice.call(args) }, //并行操作,并把所有的子线程的结果作为主线程的下一个操作的参数 paiallel : function (fns) { var self = Deferred.get(this), obj = { ok:Deferred.ok, ng:Deferred.ng, arr:[] }, count = 0, values = {} for(var key in fns){//参数可以是一个对象或数组 if(fns.hasOwnProperty(key)){ (function(key,fn){ if (typeof fn == "function") fn = Deferred(fn); fn.then(function(value){ values[key] = value; if(--count <= 0){ if(fns instanceof Array){//如果是数组强制转换为数组 values.length = fns.length; values = A_slice.call(values) } self._fire("ok",[values]) } }).once(function(e){ self._fire("ng",[e]) }); obj.arr.push(fn); count++ })(key,fns[key]) } } self.onabort = function(){ var i = 0, d; while(d = obj.arr[i++]){ d.abort(); } } self._firing.push(obj); return self }, //处理相近的迭代操作 loop : function (obj, fn, result) { obj = { begin : obj.begin || 0, end : (typeof obj.end == "number") ? obj.end : obj - 1, step : obj.step || 1, last : false, prev : null } var step = obj.step, _loop = function(i,obj){ if (i <= obj.end) { if ((i + step) > obj.end) { obj.last = true; obj.step = obj.end - i + 1; } obj.prev = result; result = fn.call(obj,i); Deferred.get(result).then(_loop).fire(i+step,obj); }else{ return result; } } return (obj.begin <= obj.end) ? Deferred.get(this).then(_loop).fire(obj.begin,obj) : null; } } //将原型方法转换为类方法 "loop wait then once paiallel".replace(/\w+/g, function(method){ Deferred[method] = Deferred.prototype[method] }); })();
Deferred提供的接口其实不算多,then once loop wait paialle就这五个,我们可以new一个实例出来,用它的实例方法,可以直接用类名加方法名,其实里面还是new了一个实例。另外,还有两个专门用于复写的方法,before与after。before执行于每个回调函数之前,after执行于所有回调之后,相当于complete了。既然是列队,就有入队操作与出队操作,我不可能使用queue与dequeue这样土的命名。queue换成两个时间状语,then与once,相当于jQuery的done、fail,或dojo的addCallback、addErrback。dequeue则用fire与error取替,jQuery1.5的beta版也曾经用过fire,不知为何换成resolve这样难记的单词。好了,我们先不管其他API,现在就试试身手吧。
var log = function (s) { window.console && window.console.log(s); } dom.Deferred(function () { log(1);//1 }) .then(function () { log(2);//2 }) .then(function () { log(3);//3 }) .fire(); //如果不使用异步列队,实现这种效果,就需要用套嵌函数 /* var fun = function(fn){ fn() }; fun(function(){ log(1); fun(function(){ log(2); fun(function(){ log(3); }) }); }); */
当然,现在是同步操作。注意,第一个回调函数是作为构造器的参数传入,可以节约了一个then^_^。
默认如果回调函数存在返回值,它会把它作为下一个回调函数的参数传入,如:
dom.Deferred(function (a) { a += 10 log(a);//11 return a }) .then(function (b) { b += 12 log(b);//23 return b }) .then(function (c) { c += 130 log(c);//153 }) .fire(1);
我们可以重载before函数,让它的结果不影响下一个回调函数。在多投事件中,我们也可以在before中定义,如果返回false,就中断队列了。
我们再来看它如何处理异常。dom.Deferred的负向列队与jQuery的是完全不同的,jQuery的只是正向列队的一个克隆,而在dom.Deferred中,负向列队只是用于突发情况,是配角。
dom.Deferred(function () { log(1111111111)//11111111111 }). then(function () { throw "error!";//发生错误 }). then(function (e) {//这个回调函数不执行 log(e+"222222") return 2222222 }). once(function(e){//直到 遇上我们自定义的负向回调 log(e+'333333')//error!333333 return 333333333 }). then(function (c) {//回到正向列队中 log(c)//33333333 }). fire()
上面几个例子严格来说是同步执行,想实现异步就要用到setTimeout。当然除了setTimeout,我们还有许多方案,img.onerror script.onreadystatechange script.onload xhr.onreadystatechange self.postMessage……但它们 都有一个缺点,就是不能指定回调函数的执行时间。更何况setTimeout是没有什么兼容问题,如img.onerrot就不能用于IE6-8,postMessage虽然很快,但只支持非常新的浏览器版本。我说过,异步就是延时,延时就是等待,因此这方法叫做wait。
dom.Deferred(function(){ log(1) }).wait(1000).then(function(){ log(2) }).wait(1000).then(function(){ log(3) }).wait(1000).then(function(){ log(4) }).fire()
好了,我们看异步列队中最难的部分,并行操作。这相当于模拟线程了,两个不相干的东西各自做自己的事,互不干扰。当然在时间我们还是能看出先后顺序来的。担当这职责的方法为paiallel。
dom.Deferred.paiallel([function(){ log("司徒正美") return 11 },function(i){ log("上官莉绮") return 12 },function(i){ log("朝沐金风") return 13 }]).then(function(d){ log(d) }).fire(10)
不过,上面并没有用到异步,都是同步,这时,paiallel就相当于一个map操作。
var d = dom.Deferred d.paiallel([ d.wait(2000).then(function(a){ log("第1个子列队"); return 123 }), d.wait(1500).then(function(a){ log("第2个子列队"); return 456 }), d.then(function(a){ log("第3个子列队") return 789 })]).then(function(a){ log(a) }).fire(3000);
最后要介绍的是loop方法,它只要改变一下就能当作animate函数使用。
d.loop(10, function(i){ log(i); return d.wait(500) });
添加多个列队,让它们交错进行,模拟“多线程”效果。
d.loop(10, function(i){ log("第一个列队的第"+i+"个操作"); return d.wait(100) }); d.loop(10, function(i){ log("第二个列队的第"+i+"个操作"); return d.wait(100) }); d.loop(10, function(i){ log("第三个列队的第"+i+"个操作"); return d.wait(100) });
当然这些例子都很简单,下次再结合ajax与动画效果说说。