javascript 异步编程总结
更新日期:
javascript一直被人诟病的就是异步操作,总是带来很多的callback形成所谓的恶魔金字塔。传统意义上的前端浏览器开发遇到的还不多,
在后端nodejs开发时,这种情况经常遇到。如何处理这种异步操作,已经成为了一个合格的前端的必修课。下面整理一下最近了解过的各种异步编程知识。
一个生活例子
假设还有1秒钟就到下班的点了,胖子虽然急着回家,但是也只能等着。
两件事:
第一件,下班。我们用个函数模拟下:
1 2 3 4 5 6 7 | function offWork(callback){ console.log("上班ing。。。") setTimeout(function(){ console.log("下班了。。。") callback(); },1000); } |
第二件,回家。模拟如下:
1 2 3 4 5 6 7 | function backHome(callback){ setTimeout(function(){ console.log("到家了!!!") callback(); },1000); console.log("回家ing。。。") } |
下班是1秒之后才发生的事情。所以 我们是不能这么干的。
1 2 | offWork()
backHome()
|
还没下班,胖子就回家了。这样就等着被骂吧。
所以我们只能乖乖的投降,慢慢的等待。于是就有了下面这样的写法。
1 2 3 | offWork(function(){
backHome()
})
|
恩看起来还不错。。是吧
但是,回家后还要吃饭,而且回家也是需要时间的。。吃饭后还要看睡觉,吃饭也是需要时间的,于是在javascript里面,我们就变成了这样写。
1 2 3 4 5 6 7 8 9 10 | offWork(function(){ backHome(function(){ eatFood(function(){ sleep(function(){ 。。。。 }) }) }) }) |
这就是恶魔金字塔问题了。
所以callback虽然可以简单的解决异步调用问题。但是异步一多,就会让人无法忍受,我们需要一些新的方式。下面就介绍几种目前比较火的方式。
事件发布订阅方式
这种方式使用一种观察者的设计模式
不知道什么是观察者模式的可以先去补补23种设计模式。建议通过java这些比较成熟的语言来了解这些模式。javascript虽然也可以实现,但个人觉得不适合初学者很好的理解。
所谓的观察者模式,是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
说白了,就是我们平时使用的事件机制。
为了更好的理解。首先我们实现一个最简单的事件监听程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | var Observer = function(){ this._callbacks = {}; this._fired = {}; }; Observer.prototype.addListener = function(eventname, callback) { this._callbacks[eventname] = this._callbacks[eventname] || []; this._callbacks[eventname].push(callback); return this; } Observer.prototype.removeListener = function(eventname,callback){ var cbs = this._callbacks,cbList,cblength; if(!eventname) return this; if(!callback){ cbs[eventname] = []; }else{ cbList = cbs[eventname]; if (!cbList) return this; cblength = cbList.length; for (var i = 0; i < cblength; i++) { if (callback === cbList[i]) { cbList.splice(i, 1); break; } } } } Observer.prototype.fire = function(eventname,data){ var cbs = this._callbacks,cbList,i,l; if(!cbs[eventname]) return this; cbList = cbs[eventname]; if (cbList) { for (i = 0, l = cbList.length; i < l; i++) { cbList[i].apply(this,Array.prototype.slice.call(arguments, 1)); } } } |
可以看到原来很简单,将事件对应的处理函数储存起来,fire的时候拿出来调用。这样一个简单的事件监听就弄好了,当然这只是个非常简陋的原型。= =就不要在意太多细节了。
现在我们可以这么写了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var observer = new Observer(); observer.addListener('backHomed',function(){ //eatFood(function(){ //..... //}); }) observer.addListener('offworked',function(){ backHome(function(){ observer.fired('backHomed'); }); }) offWork(function(){ observer.fire('offworked'); }) |
可以看到,事件监听极大的减少了各个任务之间的耦合。有效的解决了恶魔金字塔的问题。but,看着还是好刺眼啊。代码组织起来还是很吃力。
我们需要做点什么,改造下任务函数再加点扩展。扩展之后我们可以这么调用:
1 2 3 4 | var observer = new Observer(); observer.queue([offWork,backHome],function(data){ console.log("eating"); }); |
我们看下queue的扩展代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | Observer.prototype.queue = function(queue,callback){ var eventName = ''; var index= 0; var data = []; var self = this; var task = null; var _getFireCb = function(ename){ return function(val){ val = val || null; self.fire(ename,val); } } var _next = function(){ if((task = queue.shift()) != undefined){ eventName = 'queueEvent' + index++; self["addListener"](eventName, function(val){ data.push(val); _next(); }) task.call(this,_getFireCb(eventName)); }else{ callback.apply(null, [data]); } } _next(); } |
实现思路是这样的,从队列里挨个的取出task,增加事件监听,自动生成callback注入,这样task执行完后会fire一下。监听的回调函数里再调用_next拿出下个task重复流程。
有的时候我们对于顺序并不看重,比如对于吃饭这个问题,a,b,c吃饭,只要三个人都吃完了就可以去结账了。他们谁先吃完我们都不用管,如果按照上面的思路,就得a先吃,a吃完b吃,b吃完再c吃。白白浪费很多时间,我们需要发挥异步的优势,采用并行的执行方式。所以有了下面的when扩展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function aEat(callback){ setTimeout(function(){ console.log("a吃完了。。。") callback(); },1000); } function bEat(callback){ setTimeout(function(){ console.log("b吃完了。。。") callback(); },1000); } var observer = new Observer(); observer.when("a-eat-ok","b-eat-ok",function(data){ console.log("结账"); }); aEat(function(){ observer.fired('a-eat-ok'); }) bEat(function(){ observer.fired('b-eat-ok'); }); |
我们看下when的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | Observer.prototype.when = function(){ var events,callback,i,l,self,argsLength; argsLength = arguments.length; events = Array.prototype.slice.apply(arguments, [0, argsLength - 1]); callback = arguments[argsLength - 1]; if (typeof callback !== "function") { return this; } self = this; l = events.length; var _isOk = function(){ var data = []; var isok = true; for (var i = 0; i < l; i++) { if(!self._fired.hasOwnProperty(events[i])||!self._fired[events[i]].hasOwnProperty("data")){ isok = false; break; } var d = self._fired[events[i]].data; data.push(d); } if(isok) callback.apply(null, [data]); } var _bind =function(key){ self["addListener"](key, function(data){ self._fired[key] = self._fired[key] || {}; self._fired[key].data = data; _isOk(); }) } for(i=0;i<l;i++){ _bind(events[i]); } return this; } |
这段代码。其实不难,也是基于上面的事件基础上实现的。实现方法主要是对所有的事件进行监听。每个事件触发后,都会去检查其他事件是否都已经触发完毕了。如果发现都触发了就调用回调函数。当然这个扩展只适合不讲究顺序的并行执行情况。
上面的例子大部分参考eventproxy的实现,有兴趣的人可以去了解一下。
Promise 和 Defferred
Promise是一种规范,Promise都拥有一个叫做then的唯一接口,当Promise失败或成功时,它就会进行回调。它代表了一种可能会长时间运行而且不一定必须完成的操作结果。这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象。Defferred就是之后来处理回调的对象。二者紧密不可分割。
如果有了promise,我们可以这么调用上面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | function start(){ var d = new Deffered(); offWork(function(){ d.resolve('done----offWork'); }) return d.promise; } start().then(function(){ var d = new Deffered(); backHome(function(){ d.resolve('done----backhome'); }) return d.promise; }).then(function(){ /** var d = new Deffered(); eatFood(function(){ d.resolve('done----eatFood'); }) return d.promise;**/ console.log('eating'); }) |
看起来清晰多了吧。通过then可以很方便的按顺序链式调用。
下面我们来实现一个基础的promise:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | var Deffered = function(){ this.promise = new Promise(this); this.lastReturnValue = ''; } Deffered.prototype.resolve = function(obj){ var handlelist = this.promise.queue; var handler = null; //var returnVal = obj; if(obj) this.lastReturnValue = obj; this.promise.status = 'resolved'; while((handler = handlelist.shift()) != undefined){ if (handler&&handler.resolve) { this.lastReturnValue = handler.resolve.call(this,this.lastReturnValue); if (this.lastReturnValue && this.lastReturnValue.isPromise) { this.lastReturnValue.queue = handlelist; return; } } } } Deffered.prototype.reject = function(obj){ var handlelist = this.promise.queue; var handler = null; //var returnVal = obj; if(obj) this.lastReturnValue = obj; this.promise.status = 'rejected'; while((handler = handlelist.shift()) != undefined){ if (handler&&handler.reject) { this.lastReturnValue = handler.reject.call(this,this.lastReturnValue); if (this.lastReturnValue && this.lastReturnValue.isPromise) { this.lastReturnValue.queue = handlelist; return; } } } } var Promise = function(_deffered){ this.queue = []; this.isPromise = true; this._d = _deffered; this.status = 'started';//three status started resolved rejected } Promise.prototype.then = function(onfulled,onrejected){ var handler = {}; var _d = this._d; var status = this.status; if (onfulled) { handler['resolve'] = onfulled; } if (onrejected) { handler['reject'] = onrejected; } this.queue.push(handler); if (status == 'resolved') _d.resolve(); if (status == 'rejected') _d.reject(); return this; } |
首先我们先看promise部分,Promise有三种状态。未完成(started),已完成(resolved),失败(rejected)。Promise只能是由未完成往 另外两种状态转变,而且不可逆。
我们先是定义了一个队列,用来存放所有的回调函数包括正确完成的回调(onfulled)和失败的回调(onrejected)。this.isPromise = true;
用来表明是一个promise对象。this._d = _deffered;
是用来存储与这个promise对象对应的deffered对象的。
deffered对象一般具有resolve还有reject方法分别代表开始执行队列里handle相应的回调。
promise有一个then方法,用来声明完成的函数,还有失败的函数。
1 2 3 | this.queue.push(handler); if (status == 'resolved') _d.resolve(); if (status == 'rejected') _d.reject(); |
这段代码先是将回调对象储存起来,后面的两个判断,是用来当一个promise对象已经不是未完成时直接调用then添加的回调。
下面我们看下Deffered对象,首先有个promise对象的引用。还有个lastReturnValue,这个是用来储存promise队列里面的handle回调的返回值的。
我们重点看下Deffered.prototype.resolve
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | Deffered.prototype.resolve = function(obj){ var handlelist = this.promise.queue; var handler = null; //var returnVal = obj; obj && this.lastReturnValue = obj; this.promise.status = 'resolved'; while((handler = handlelist.shift()) != undefined){ if (handler&&handler.resolve) { this.lastReturnValue = handler.resolve.call(this,this.lastReturnValue); if (this.lastReturnValue && this.lastReturnValue.isPromise) { this.lastReturnValue.queue = handlelist; return; } } } } |
还记得我们怎么调用的吗?
没错,我们先要创建一个deffered对象,之后返回他的promise对象。通过then,我们给这个promise添加了很多的异步正确完成回调。同时这些回调也返回自己的promise对象。此时backHome对应的deffered对象关联的promise里面已经通过then添加了很多回调函数。但是并未执行。
在start函数里面当backhome完成时 我们执行了d.resolve('done----backhome');
这个 时候调用了backHome对应的deffered对象的resolve。
1 2 3 4 5 6 7 8 9 10 | while((handler = handlelist.shift()) != undefined){ if (handler&&handler.resolve) { this.lastReturnValue = handler.resolve.call(this,this.lastReturnValue); if (this.lastReturnValue && this.lastReturnValue.isPromise) { this.lastReturnValue.queue = handlelist; return; } } } |
backHome对应的deffered对象的resolve里面开始循环调用回调队列里的函数。同时backHome对应的deffered对象关联的promise的状态已经变成了已完成。
请注意下面这个判断:
1 2 3 4 | if (this.lastReturnValue && this.lastReturnValue.isPromise) { this.lastReturnValue.queue = handlelist; return; } |
当then添加的是一个普通非异步函数时。就会继续取出队列的函数执行。但是当添加的函数也返回了一个promise,这时候话语权就要交给这个新的promise了,当前队列的执行就要停下来,同时将当前的操作函数队列赋值给新的peomise的队列,完成交接。之后就又是一个新的promise从未完成到另外状态的过程了,只有新的promise被resolve或者reject了,下面的才会继续执行下去。
可以看到通过promise和deffered,事件的声明和调用完全分开了。一个负责管理函数一个负责调用。非常灵活优雅。
promise与很多开源库实现了,比较出名的是when.js,Q,有兴趣的可以去了解下。
尾触发机制
这是connect中间件使用的方式,可以串行处理异步代码。当然这只是一种实现思路,不具备通用性,所有任务都需要一个next参数。我们需要对前面的代码做些小改造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | function offWork(data,next){ console.log("上班ing。。。") setTimeout(function(){ console.log("下班了。。。") next('传给下个任务的数据'); },1000); } function backHome(data,next){ console.log('上个任务传过来的数据为:'+data); setTimeout(function(){ console.log("到家了!!!") next('传给下个任务的数据'); },1000); console.log("回家ing。。。") } App = { handles:[], use:function(handle){ if(typeof handle == 'function') App.handles.push(handle); }, next:function(data){ var handlelist = App.handles; var handle = null; var _next = App.next; if((handle = handlelist.shift()) != undefined){ handle.call(App,data,_next); } }, start:function(data){ App.next(data); } } |
每个任务,都必须有两个参数,next是一个函数引用,等当前任务结束时,需要手动调用next,就可以启动下一个任务的运行,当然可以通过next(data)传一些数据给下一个任务。任务的第一个参数就是上一个任务调next的时候传过来的数据。
于是我们可以这么调用了:
1 2 3 4 | App.use(offWork); App.use(backHome); App.start(); |
显然调用过程非常直观,这个方式的缺点就是需要对每个任务进行相应的改造。而且只能是串行的执行,不能很好的发挥异步的优势。
wind.js
还有种比较知名的方式,是国内的程序员老赵的wind.js,它使用了一种完全不同的异步实现方式。前面的所有方式都要改变我们正常的编程习惯,但是wind.js不用。它提供了一些服务函数使得我们可以按照正常的思维去编程。
下面是一个简单的冒泡排序的算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | var compare = function (x, y) { return x - y; } var swap = function (a, i, j) { var t = a[i]; a[i] = a[j]; a[j] = t; } var bubbleSort = function (array) { for (var i = 0; i < array.length; i++) { for (var j = 0; j < array.length - i - 1; j++) { if (compare(array[j], array[j + 1]) > 0) { swap(array, j, j + 1); } } } } |
很简单就不讲解了,现在的问题是我们如果要做一个动画,一点点的展示这个过程呢。
于是我们需要给compare加个延时,并且swap后重绘数字展现。
可javascript是不支持sleep这样的休眠方法的。如果我们用setTimeout模拟,又不能保证比较的顺序的正确执行。
可是有了windjs后我们就可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var compareAsync = eval(Wind.compile("async", function (x, y) { $await(Wind.Async.sleep(10)); // 暂停10毫秒 return x - y; })); var swapAsync = eval(Wind.compile("async", function (a, i, j) { $await(Wind.Async.sleep(20)); // 暂停20毫秒 var t = a[i]; a[i] = a[j]; a[j] = t; paint(a); // 重绘数组 })); var bubbleSortAsync = eval(Wind.compile("async", function (array) { for (var i = 0; i < array.length; i++) { for (var j = 0; j < array.length - i - 1; j++) { // 异步比较元素 var r = $await(compareAsync(array[j], array[j + 1])); // 异步交换元素 if (r > 0) $await(swapAsync(array, j, j + 1)); } } })); |
注意其中最终要的几个辅助函数:
- eval(Wind.compile(“async”, func) 这个函数用来定义一个“异步函数”。这样的函数定义方式是“模板代码”,没有任何变化,可以认做是“异步函数”与“普通函数”的区别。
- Wind.Async.sleep() 这是windjs对于settimeout的一个封装,就是用上面的 eval(Wind.compile来定义的。
- $await()所有经过定义的异步函数,都可以使用这个方法 来等待异步函数的执行完毕。
这样上面的代码就可以很容易的理解了。compare,swap都被弄成了异步函数,然后使用$await等待他们的执行完毕。可以看到跟我们之前的写法比起来,实现思路几乎一样,只是多了些辅助函数。相当的创新。
windjs的实现原理,暂时没怎么看,这是一种预编译的思路。之后有空看看也来实现一个简单的demo。
generator and co
什么是generator?generator是javascript1.7的内容,是 ECMA-262 在第六个版本,即我们说的 Harmony 中所提出的新特性。所以,没错这个特性支持很一般。
有下面几种办法体验generator:
- node v0.11 可以使用 (node —harmony)
- 使用gnode来使用,不过据说性能一般
- 使用chrome体验,打开chrome://flags/, 搜索harmony, 启用, 重启chrome即可。
我们看个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function* start() {
var a = yield 'start';
console.log(a);
var b = yield 'running';
console.log(b);
var c = yield 'end';
console.log(c);
return 'over';
}
var it = start();
console.log(it.next(11));//Object {value: "start", done: false}
console.log(it.next(22));//22 object {value: 'running', done: false}
console.log(it.next(333));//333 Object {value: 'end', done: false}
console.log(it.next(444));//444 Object {value: "over", done: true}
|
其实很好理解,function* functionname() {
用来声明一个generator function
。通过执行generator function
我们得到一个generator
,也就是it。
当我们调用it.next(11)的时候,代码会执行到var a = yield 'start';
然后断点。注意这个时候还没有进行对a的赋值,这个时候it.next(11)返回一个对象有两个属性,value代表yield返回的东西,可以是值也可以是函数。done代表当前generator有没有结束。
当我们调用 it.next(22)的时候,代码开始执行到var b = yield running;
。此时你发现打出了22,没错a的值被赋为22,也就是说next里面的参数会作为上一个yield的返回值。
一直到调用it.next(444),代码一直执行到return,这个时候 函数的返回值就作为 next返回对象的value值,也就是我们的over。
这就是generator的全部内容了
详细的可以参考这边的MDN的介绍,猛戳这里
那我们如何将它应用在我们的异步代码上呢?
实际上TJ大神已经做了这件事,编写了一个CO的库。
我们简单探讨下CO的原理
假设我们需要知道小胖回家的总时间。
有了co框架后 我们可以这么完成我们上面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | function offWork(callback){ console.log("上班ing。。。") setTimeout(function(){ console.log("下班了。。。") callback(1); },1000); } function backHome(callback){ setTimeout(function(){ console.log("到家了!!!") callback(2); },2000); console.log("回家ing。。。") } co(function* () { var a; a = yield offWork; console.log(a); a = yield backHome; console.log(a); })(function(data) { console.log(data); }) //结果为: /* 上班ing。。。 下班了。。。 1 回家ing。。。 到家了!!! 2 2 */ |
co函数接收一个generatorfunction作为参数,生成一个实际操作函数。这个实际操作函数可以接收一个callback来传入最后一个异步任务的回调值。
可以看到我们可以直接使用a = yield offWork;
来获取异步函数offwork的返回值。真的是太赞了,而且我们可以提供一个回调用来接收最后回调的值,这边就是backHome回调的值。
下面我们来实现这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function *co(generatorFunction){ var generator = generatorFunction(); return function(cb){ var iterator = null; var _next = function (args){ iterator = generator.next(args); if(iterator.done){ cb&&cb(args); }else{ iterator.value(_next); } } _next(); } } |
代码很简单,就是不停的调用generator的next,当next返回的对象的done属性不为空时就执行返回的异步函数。注意那边args的传递。
可以看到短短几行就实现了这个功能,当然实际的co框架比这个复杂的多,这边只是实现了最基础的原理。
使用co时,yield的必须是thunk函数,thunk函数就是那种参数只有一个callback的函数,这个可以使用一些方法转换,也有一些库支持,可以了解下thunkify 或者thunkify-wrap。
这边给个简单的普通nodejs读文件函数到thunk函数的转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 | function read(file) { return function(fn){ fs.readFile(file, 'utf8', fn); } } //于是可以这么用 co(function* () { var a; a = yield read('.gitignore'); })(function(data) { }) |
结语
javascript是一门短时间内就创出的语言,虽然很灵活,但是很容易写出糟糕的代码。异步编程,在性能问题上尤其是io处理上是它的优势,但是同时也是它的劣势,大部分人都无法很好的组织异步代码。于是就出现了一大堆的库,来给它擦屁股。不得不说人类的智慧是无限的。上面这么多的异步流程库的实现就是很好的例子,没有最好的语言,只有最合适的。也没有最好的异步实现方式,关键是找到合适的。
除了上面介绍的这些实现异步编程的思路以外,其实还有很多优秀的实现方式,以后有空再研究下step,async等等的实现方式。