javascript 异步编程总结
purplebamboo opened this issue · comments
javascript 异步编程总结
javascript一直被人诟病的就是异步操作,总是带来很多的callback形成所谓的恶魔金字塔。传统意义上的前端浏览器开发遇到的还不多,
在后端nodejs开发时,这种情况经常遇到。如何处理这种异步操作,已经成为了一个合格的前端的必修课。下面整理一下最近了解过的各种异步编程知识。
##一个生活例子
假设还有1秒钟就到下班的点了,胖子虽然急着回家,但是也只能等着。
两件事:
第一件,下班。我们用个函数模拟下:
function offWork(callback){
console.log("上班ing。。。")
setTimeout(function(){
console.log("下班了。。。")
callback();
},1000);
}
第二件,回家。模拟如下:
function backHome(callback){
setTimeout(function(){
console.log("到家了!!!")
callback();
},1000);
console.log("回家ing。。。")
}
下班是1秒之后才发生的事情。所以 我们是不能这么干的。
offWork()
backHome()
还没下班,胖子就回家了。这样就等着被骂吧。
所以我们只能乖乖的投降,慢慢的等待。于是就有了下面这样的写法。
offWork(function(){
backHome()
})
恩看起来还不错。。是吧
但是,回家后还要吃饭,而且回家也是需要时间的。。吃饭后还要看睡觉,吃饭也是需要时间的,于是在javascript里面,我们就变成了这样写。
offWork(function(){
backHome(function(){
eatFood(function(){
sleep(function(){
。。。。
})
})
})
})
这就是恶魔金字塔问题了。
所以callback虽然可以简单的解决异步调用问题。但是异步一多,就会让人无法忍受,我们需要一些新的方式。下面就介绍几种目前比较火的方式。
##事件发布订阅方式
这种方式使用一种观察者的设计模式
不知道什么是观察者模式的可以先去补补23种设计模式。建议通过java这些比较成熟的语言来了解这些模式。javascript虽然也可以实现,但个人觉得不适合初学者很好的理解。
所谓的观察者模式,是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时, 所有依赖于它的对象都得到通知并被自动更新。
说白了,就是我们平时使用的事件机制。
为了更好的理解。首先我们实现一个最简单的事件监听程序。
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的时候拿出来调用。这样一个简单的事件监听就弄好了,当然这只是个非常简陋的原型。= =就不要在意太多细节了。
现在我们可以这么写了:
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,看着还是好刺眼啊。代码组织起来还是很吃力。
我们需要做点什么,改造下任务函数再加点扩展。扩展之后我们可以这么调用:
var observer = new Observer();
observer.queue([offWork,backHome],function(data){
console.log("eating");
});
我们看下queue的扩展代码:
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扩展。
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的实现方式:
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,我们可以这么调用上面的例子:
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:
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方法,用来声明完成的函数,还有失败的函数。
this.queue.push(handler);
if (status == 'resolved') _d.resolve();
if (status == 'rejected') _d.reject();
这段代码先是将回调对象储存起来,后面的两个判断,是用来当一个promise对象已经不是未完成时直接调用then添加的回调。
下面我们看下Deffered对象,首先有个promise对象的引用。还有个lastReturnValue,这个是用来储存promise队列里面的handle回调的返回值的。
我们重点看下Deffered.prototype.resolve
:
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。
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的状态已经变成了已完成。
请注意下面这个判断:
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参数。我们需要对前面的代码做些小改造。
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的时候传过来的数据。
于是我们可以这么调用了:
App.use(offWork);
App.use(backHome);
App.start();
显然调用过程非常直观,这个方式的缺点就是需要对每个任务进行相应的改造。而且只能是串行的执行,不能很好的发挥异步的优势。
##wind.js
还有种比较知名的方式,是国内的程序员老赵的wind.js,它使用了一种完全不同的异步实现方式。前面的所有方式都要改变我们正常的编程习惯,但是wind.js不用。它提供了一些服务函数使得我们可以按照正常的思维去编程。
下面是一个简单的冒泡排序的算法:
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后我们就可以这么写:
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即可。
我们看个简单的例子:
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框架后 我们可以这么完成我们上面的代码:
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回调的值。
下面我们来实现这个函数:
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函数的转换。
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等等的实现方式。
该补充 async了。。。
http://yanhaijing.com/javascript/2017/08/02/talk-async/