A-A+

javascript 异步编程总结

2015年03月23日 JavaScript 暂无评论 阅读 1,060 次
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));
        }
    }
}));

注意其中最终要的几个辅助函数:

  1. eval(Wind.compile(“async”, func) 这个函数用来定义一个“异步函数”。这样的函数定义方式是“模板代码”,没有任何变化,可以认做是“异步函数”与“普通函数”的区别。
  2. Wind.Async.sleep() 这是windjs对于settimeout的一个封装,就是用上面的 eval(Wind.compile来定义的。
  3. $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等等的实现方式。

标签:
Copyright © 互联网世界 保留所有权利.   Powered by www.zhangjinpeng.com.cn 网站地图   粤ICP备13066957号-2  
内容说明:本站内容及数据部分来自互联网及公开渠道,如有侵权请及时联系我们,本站将在第一时间删除相关资源。

用户登录

分享到: