深刻浅出 ES6-Generator

深刻浅出 ES6-Generator

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


功能需求

咱们如今要作的,是一个基于nodejs+mongoDB的web应用,其中须要一个web页面,显示用户的列表。就这样,需求就这么简单。web

编码实现

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

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 = ...
	})
});

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

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,返回操做是由代码声明,那么就能够在回调中才进行返回操做;而使用了koa,则是由框架执行route handler(定制代码),而后由框架执行返回。异步

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

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

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');
}

就是顺序打印一、二、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的博客查看。

相关文章
相关标签/搜索