深刻淺出 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的博客查看。

相關文章
相關標籤/搜索