深入浅出 ES6-Generator

原创
2015/07/21 17:40
阅读数 620

深入浅出 ES6-Generator

先不谈概念、定义,我们从实际用例切入。


功能需求

我们现在要做的,是一个基于nodejs+mongoDB的web应用,其中需要一个web页面,显示用户的列表。就这样,需求就这么简单。

编码实现

首先,我们选用了koa作为web层的框架,并且使用mongodb的原生driver访问数据库。

var koa = require('koa');
var route = require('koa-route');
var mongo = require('mongodb').MongoClient;

var app = koa();
// 对/user地址的请求,会被路由到userlist这个函数处理
app.use(route.get('/user',userlist));

app.listen(8002);

function* userlist() {
	var _this = this;
	
	// 先连接mongodb,这是异步IO的过程,所以要回调
	mongo.connect('mongodb://localhost/koa-test', function(err, db){
		// 连接数据库成功后,获取user这个集合
	    var users = db.collection('user');
	    // 对user集合进行查询,这也是一个异步IO过程,所以也产生回调
	    users.find({}).toArray(function(err, docs) {
		    // 查询完毕,可以关闭数据库连接
	        db.close();
	        
	        // 将查询结果写到返回的body中
	        _this.body = '';
		    for(var i=0; i<docs.length; i++) {
		        _this.body += '<h2>' + docs[i].username + '</h2>';
		    }
		});
	});
}

上述代码,有两个异步过程,因此产生两个回调函数。为确保异步操作正确完成,最终对返回body的赋值要放到最里面的回调中:

mongo.connect(url, function(){
	...
    users.find().toArray(function(){
		...
		_this.body = ...
	})
});

但代码实际运行还是会报错:

Error: Can't set headers after they are sent.

这依然是异步造成的问题:koa框架对userlist函数的执行,是不会被connect等异步操作所阻塞,也即mongo.connect触发之后,userlist函数会直接往下走,koa发现没有逻辑了,就把请求返回了。而等mongo.connect、users.find完成后(进入回调函数),请求早已返回,此时对body和header的赋值都将是非法的。

当然,这个问题,也是由于使用koa造成的:如果不实用koa,返回操作是由代码声明,那么就可以在回调中才进行返回操作;而使用了koa,则是由框架执行route handler(定制代码),然后由框架执行返回。

要解决这个问题,就要把异步非阻塞,变成异步阻塞

这个时候,终于入正题了:ES6-Generator

Generator是什么暂且不详述,我们先看看它如何为我们解决问题:

var koa = require('koa');
var route = require('koa-route');
var monk = require('monk');
var wrap = require('co-monk');

var app = koa();
// 对/user地址的请求,会被路由到userlist这个函数处理
app.use(route.get('/user',userlist));

app.listen(8002);

function* userlist() {
	// 连接数据库,非阻塞的异步过程
	var db = monk('localhost/koa-test');
	// 获取user集合
    var users = wrap(db.get('user'));
    // 对user集合进行查询,非阻塞异步过程
    var userlist = yield users.find({});

	// 将查询结果写到返回的body中
	this.body = '';
	for(var i=0; i<userlist.length; i++) {
        this.body += '<h2>' + userlist[i].username + '</h2>';
    }
    db.close();
}

和之前的代码最大的区别,是两个异步操作(数据库连接,集合查询)的回调函数没有了,代码以更符合人类思维的顺序执行方式,也即代码从横向扩展变成了竖向扩展。

其中,我们要集中看的是这句

var userlist = yield users.find({});

这里在执行users.find之前,多了一个关键字yield。就是它,让我们的异步代码有了阻塞执行的功能。 如果这里我们省去yield,变成这样:

var userlist = users.find({});

代码执行是会报错:

TypeError: Cannot read property 'username' of undefined

这说明,没有yield关键字的时候,users.find异步非阻塞的执行,在等待磁盘IO的时候,下面的逻辑已经在执行

    for(var i=0; i<userlist.length; i++) {
        this.body += '<h2>' + userlist[i].username + '</h2>';
    }

此时查询的磁盘IO还没完成,userlist变量就还没被赋值,所以是undefined,就出现上述的报错了。

至于这一句:

var db = monk('localhost/koa-test');

之所以不需要使用yield关键字,是因为我们使用了monk这个库,monk内部已经进行了包装,所以在外部调用的时候无须加上yield,具体这里不详述。

至此,我们带出的是ES6-Generator中非常重要的yield关键字。要理解generator,必须理解这个yield,除了上面提到的,yield会让异步非阻塞代码变成异步阻塞代码,其实还有一个更好理解的比喻:断点

看这样一个示例代码:

function test() {
	console.log('1');
	console.log('2');
	console.log('3');
}

就是顺序打印1、2、3,简单到没朋友。加上yield之后呢?

function* test() {
	console.log('1');
	yield 1;
	console.log('2');
	yield 2;
	console.log('3');
}

代码中间插了两行yield,代表什么呢?

  • 当test执行到 yield 1这一行的时候,程序将被挂起,要等待执行下一步的指令;
  • 当接收到指令后,test将继续往下运行,直到yield 2这一行,然后程序又被挂起并等待指令;
  • 收到指令后,test又将继续运行,而下面已经没有yield了,那么函数运行结束。

这是不是就像,我们调试代码的时候,给插的断点

当然,断点这个比喻,只是表象上比较相像,实质原理还是有非常大差异。

要注意,function后面多了一个星号,这样是表明这个函数将变成一个生成器函数,而不是一个普通函数了。意思就是,test这个函数,将不能被这样执行

test();

但可以获得一个生成器

var gen = test();  // gen就是一个生成器了

然后,生成器可以通过next()来执行运行

gen.next();

也就是上面说的,让函数继续运行的指令。

简单地总结一下:

  • 生成器通过yield设置了一些类似”断点“的东西,使得函数执行到yield的时候会被阻断;
  • 生成器要通过next()指令一步一步地往下执行(两个yield之间为一步);
  • yield 语句后面带着的表达式或函数,将在阻断之前执行完毕;
  • yield 语句下面的代码,将不可能在阻断之前被执行;

由此可以看出,yield是如何将异步非阻塞代码,变成 异步阻塞代码。

P.S. generator是要配合执行器使用的,回顾mongodb的示例代码中并没有使用执行器,这是因为使用了koa框架。koa自封装了一个co作为generator的执行器,在koa框架下generator会被co自动执行,所以开发者无需关注这些细节,也因此代码变得更为简洁。


generator是ES6里面一个很重要的部分,目的就是解决JS异步代码带来的问题。generator包含很多概念,如上述的执行器,本文只是从应用场景出发,简单解释了generator是如何解决这些异步代码问题的,里面更深层次的原理未有完整覆盖,如果需要,可以搜索ruanyf的博客查看。

展开阅读全文
打赏
1
3 收藏
分享
加载中
很棒。
2018/02/15 09:40
回复
举报
更多评论
打赏
1 评论
3 收藏
1
分享
返回顶部
顶部