深入浅出Node.js读书笔记:异步编程解决方案(4.3)

之前的文章提到,异步编程带来的各种问题。本节将通过提供三种解决方案来解决这些问题。

解决方案有如下3种。
1.事件发布/订阅模式。
2.Promise/Deferred模式。
3.流程控制库。

4.3.1 事件发布/订阅模式

事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又称发布/订阅模式。

Node自身提供的events模块是发布/订阅模式的一个简单实现。它具有addListener/on(),once(),removeListener(),removeAllListeners()和emit()等基本的事件监听模式的方法实现。

事件发布/订阅模式的操作极其简单,示例代码如下:

// 订阅
emitter.on("event1", function (message) { 
	console.log(message); 
}); 

// 发布
emitter.emit('event1', "I am message!");

通过emit()发布事件后,消息会立即传递给当前事件的所有监听器执行。监听器可以很灵活地添加和删除,使得事件和具体处理逻辑之间可以很轻松地关联和解耦。

在Node中,emit()调用多半是伴随事件循环而异步触发的,所以说事件发布/订阅广泛应用于异步编程。

从另一个角度看,事件监听模式也是一种钩子机制,利用钩子导出内部数据或状态给外部的调用者。

Node中大多对象具有黑盒的特点,功能较少,如果不通过事件钩子的形式,无法获取对象在运行期间的中间值或内部状态。通过钩子可以关注组件是如何启动和执行的。

1) 继承events模块

实现一个继承EventEmitter的类是十分简单,以下代码是Node中Stream对象继承EventEmitter的样子。


var events = require('events'); 

function Stream() { 
	events.EventEmitter.call(this); 
}

util.inherits(Stream, events.EventEmitter);
 

Node在util模块中封装了继承的方法,所以此处可以很便利地调用。开发者可以通过这样的方式轻松继承EventEmitter类,利用事件机制解决业务问题。在Node提供的核心模块中,有近半数都继承自EventEmitter。

2) 利用事件队列解决雪崩问题

在事件订阅/发布模式中,通常也有一个once()方法,通过它添加的监听器只能执行一次,在执行之后就会将它与事件的关联移除。这个特性常常可以帮助我们过滤一些重复性的事件响应。

下面介绍如何采用once()来解决雪崩问题。

所谓雪崩问题,就是在高访问量,大并发量的情况下缓存失效的情景,此时大量的请求同时涌入数据库中,数据库无法同时承担如此大的查询请求,进而影响到网站整体的响应速度。

如下是一条数据库查询语句的调用:

	var select = function (callback) { 
	 	db.select("SQL", function (results) { 
	 		callback(results); 
	 	}); 
	};

如果站点刚启动,此时缓存中是不存在数据的,而如果访问量巨大,同一句SQL会被发送到数据库中反复查询,会影响服务的整体性能。

这时可以引入事件队列,代码如下:

var proxy = new events.EventEmitter(); 
	var status = "ready"; 
	var select = function (callback) { 
 		proxy.once("selected", callback); 
 		if (status === "ready") { 
 			status = "pending"; 
 			db.select("SQL", function (results) { 
 				proxy.emit("selected", results); 
 			status = "ready"; 
 		}); 
 	} 
};

这里利用once()方法,将所有请求的回调都压入事件队列中,利用其执行一次就会将监视器移除的特点,保证每一个回调只会执行一次。对于相同的SQL语句,保证在同一个查询开始到结束的过程中永远只有一次。

4.3.2 Promise/Deferred模式

使用事件的方式时,执行流程需要被预先设定。即便是分支,也需要预先设定,这是由发布/订阅模式的运行机制所决定的。

下面为普通的Ajax调用:

	$.get('/api', { 
 		success: onSuccess, 
 		error: onError, 
 		complete: onComplete 
	});

在上面的异步调用中,必须严谨地设置目标。那么是否有一种先执行异步调用,延迟传递处理的方式呢?答案是Promise/Deferred模式。

Promise/Deferred模式在Javascript框架中最早出现于Dojo的代码中,被广为所知则来自于JQuery1.5版本,该版本几乎重写了Ajax部分,使得调用Ajax时可以通过如下的形式进行:

$.get(’/api’).success(onSuccess).error(onError).complete(onComplete);

这使得即使不调用success(),error()等方法,Ajax也会进行,这样的调用方式比预先传入回调让人觉得舒适一些。在原始的API中,一个事件只能处理一个回调,而通过Deferred对象,可以对事件加入任意的业务处理逻辑。

异步的广度使用使得回调,嵌套出现,但是一旦出现深度的嵌套,就会让编程的体验变得不愉快,而Promise/Deferred模式在一定程度上缓解了这个问题。

Promise/Deferred模式在2009年被KrisZyp抽象为一个提议草案,发布在CommonJS规范中。

CommonJS草案目前已经抽象出了Promise/A,Promise/B,Promise/C这些典型的异步Promise/Deferred模型,这使得异步操作可以以一种优雅的方式出现。

这里着重介绍Promise/A来介绍Promise/Deferred模式

该模式主要包含两部分,即Promise和Deferred。这里暂不提两者的区别是什么,先看看Promise/A的行为。

Promise操作只会处在3中状态的一种:未完成态,完成态和失败态。

Promise的状态只会出现从未完成态向完成态或失败态转化,不能逆转。完成态和失败态不能相互转化。

Promise的状态一旦转化,将不能被更改。

为了演示Promise/A提议,这里我们尝试通过继承Node的events模块来完成一个简单的实现,代码如下:

	
	var Promise = function () { 
 		EventEmitter.call(this); 
	}; 

	util.inherits(Promise, EventEmitter); 
	
	Promise.prototype.then = function (fulfilledHandler, 			
		errorHandler, progressHandler) { 
 	
	 	if (typeof fulfilledHandler === 'function') { 
	 		this.once('success', fulfilledHandler); 
	 	} 
	 
	 	if (typeof errorHandler === 'function') { 
	 		this.once('error', errorHandler); 
	 	} 
	 
	 	if (typeof progressHandler === 'function') { 
	 		this.on('progress', progressHandler); 
	 	} 
 		return this; 
	};
	

这里看到then()方法所做的事情是将回调函数存起来。为了完成整个流程,还需要触发执行这些回调函数的地方,实现这些功能的对象被称为Deferred,即延迟对象。示例代码如下:

	
	var Deferred = function () { 
 		this.state = 'unfulfilled'; 
 		this.promise = new Promise(); 
	}; 
	
	Deferred.prototype.resolve = function (obj) { 
 		this.state = 'fulfilled'; 
 		this.promise.emit('success', obj); 
	}; 

	Deferred.prototype.reject = function (err) { 
 		this.state = 'failed'; 
 		this.promise.emit('error', err); 
	}; 

	Deferred.prototype.progress = function (data) { 
 		this.promise.emit('progress', data); 
	};
	

Deferred里的resolve和reject方法代表状态的成功与失败,即完成态和失败态。

Promise中的多异步协作

在Promise的介绍中说过,主要解决的是单个异步操作中存在的问题。回到我们的难点,当我们需要处理多个异步调用时,又该如何处理呢?

这里给出一个简单的原型实现,相关代码如下:

	
	Deferred.prototype.all = function (promises) {
		var count = promises.length; 
 		var that = this; 
 		var results = []; 
 		promises.forEach(function (promise, i) { 
	 		promise.then(function (data) { 
		 		count--; 
		 		results[i] = data; 
		 		if (count === 0) { 
				that.resolve(results); 
	 			} 
	 		}, function (err) { 
		 		that.reject(err); 
	 		}); 
 		}); 
 		return this.promise; 
	};
	

对于多次文件的读取场景,以下面的代码为例,all()方法将两个独立的Promise重新抽象组合成一个新的Promise:

	
	var promise1 = readFile("foo.txt", "utf-8"); 
	var promise2 = readFile("bar.txt", "utf-8"); 
	
	var deferred = new Deferred(); 
	
	deferred.all([promise1, promise2]).then(function (results) 
	{ 
 		// TODO
	}, function (err) { 
 		// TODO
	});
	

这里通过all()方法抽象多个异步操作。只有所有异步操作成功,这个异步操作才算成功,一旦其中一个异步操作失败,整个异步操作就失败。

4.3.3 流程控制库

前面介绍了最为流行的模式----事件发布/订阅和Promise/Deferred模式,这些经典的模式或者是写进规范里的解决方案,但一旦涉及模式或者规范,就需要为它们做较多的准备工作,这节主要讲非模式化的应用。

这里主要介绍下async,async长期占据NPM依赖榜的前三名,可以在Node开发中,流程控制是开发过程中的基本需求。async模块提供20多个方法用于处理异步的各种协作模式,这里我们介绍几种经典用法。

1)异步的串行执行

async提供了series()方法来实现一组任务的串行执行,代码如下:


	async.series([ 
 		function (callback) { 
 		fs.readFile('file1.txt', 'utf-8', callback); 
 	}, 
 	function (callback) { 
 		fs.readFile('file2.txt', 'utf-8', callback); 
 	} 	 
	], function (err, results) { 
 		// results => [file1.txt, file2.txt] 
	});

这段代码等价于:

	
	fs.readFile('file1.txt', 'utf-8', function (err, content) { 
	 	if (err) { 
	 		return callback(err); 
	 	} 
	 	fs.readFile('file2.txt ', 'utf-8', function (err, data) 		
		{ 
	 		if (err) { 
	 			return callback(err); 
	 		} 
	 		callback(null, [content, data]); 
	 	}); 
	});
	

这里每个callback()执行时会将结果保存起来,然后执行一下调用,知道结束所有调用。最终的回调函数执行时,队列里的异步调用保存的结果以数组的方式传入。这里的异常处理规则是一旦出现异常,就结束所有调用,并将异常传递给最终回调函数的第一个参数。

2)异步的并行执行

当我们需要通过并行来提升性能时,async提供了parallel()方法,用以并行执行一些异常操作。以下为读取两个文件的并行版本:


	async.parallel([ 
 		function (callback) { 
 		fs.readFile('file1.txt', 'utf-8', callback); 
 	}, 
 	function (callback) { 
 		fs.readFile('file2.txt', 'utf-8', callback); 
 	} 
	], function (err, results) { 
 		// results => [file1.txt, file2.txt] 
	});
	

上面代码等同于下面代码:

	
	var counter = 2; 
	var results = []; 
	var done = function (index, value) { 
 		results[index] = value; 
 		counter--; 
 		if (counter === 0) { 
 			callback(null, results); 
 		} 
	};

	var hasErr = false; 
	var fail = function (err) { 
 		if (!hasErr) { 
	 		hasErr = true; 
 			callback(err); 
 		} 
	}; 
	
	fs.readFile('file1.txt', 'utf-8', function (err, content) { 
 		if (err) { 
 			return fail(err); 
 		} 
 		done(0, content); 
	}); 

	fs.readFile('file2.txt', 'utf-8', function (err, data) { 
 		if (err) { 
 			return fail(err); 
 		} 
 		done(1, data); 
	});
	

通过async编写的代码既没有深度的嵌套,也没有复杂的状态判断,它的诀窍依然来自于注入的回调函数。parallel()方法对于异常的判断依然是一旦某个异步调用产生了异常,就会将异常作为第一个参数传入给最终的回调函数。只有所有异步调用都正常完成时,才会将结果以数组的方式传入。

3)异步调用的依赖处理

series()适合无依赖的异步串行执行,但当前一个的结果是后一个调用的输入时,series()方法就无法满足需求了。所幸,这种典型场景的需求,async提供了waterfall()方法来满足,相关代码如下:

	
	async.waterfall([ 
 		function (callback) { 
 			fs.readFile('file1.txt', 'utf-8', function (err, content) { 
 			callback(err, content); 
 		}); 
 	}, 
 
 	function (arg1, callback) { 
 		// arg1 => file2.txt 
 		fs.readFile(arg1, 'utf-8', function (err, content) { 
 			callback(err, content); 
 		}); 
 	}, 
 
 	function(arg1, callback){ 
 		// arg1 => file3.txt 
 		fs.readFile(arg1, 'utf-8', function (err, content) { 
 			callback(err, content); 
 		}); 
 	} 
	], function (err, result) { 
 		// result => file4.txt 
	});	
	

这段代码等价于如下代码:

	
	fs.readFile('file1.txt', 'utf-8', function (err, data1) { 
	 	if (err) { 
	 		return callback(err); 
	 	} 
	 	
	 	fs.readFile(data1, 'utf-8', function (err, data2) { 
			if (err) { 
		 		return callback(err); 
		 	} 
		 
		 	fs.readFile(data2, 'utf-8', function (err, data3) { 
		 		if (err) { 
		 			return callback(err); 
		 		} 
		 		callback(null, data3); 
		 	}); 
 		}); 
	});
	

最后总结这几种方案的区别:事件发布/订阅模式相对算是一种较为原始的方式,Promise/Deferred模式贡献了一个非常不错的异步任务模型的抽象。而上述的这些异步流程控制方法与Promise/Deferred模式的思路不同,Promise/Deferred的重头在于封装异步的调用部分,流程控制库则显得没有模式,将处理重点放置在回调函数的注入上。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值