先不談概念、定義,咱們從實際用例切入。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,表明什麼呢?
這是否是就像,咱們調試代碼的時候,給插的斷點 ?
固然,斷點這個比喻,只是表象上比較相像,實質原理仍是有很是大差別。
要注意,function後面多了一個星號,這樣是代表這個函數將變成一個生成器函數,而不是一個普通函數了。意思就是,test這個函數,將不能被這樣執行
test();
但能夠得到一個生成器
var gen = test(); // gen就是一個生成器了
而後,生成器能夠經過next()來執行運行
gen.next();
也就是上面說的,讓函數繼續運行的指令。
由此能夠看出,yield是如何將異步非阻塞代碼,變成 異步阻塞代碼。
P.S. generator是要配合執行器使用的,回顧mongodb的示例代碼中並無使用執行器,這是由於使用了koa框架。koa自封裝了一個co做爲generator的執行器,在koa框架下generator會被co自動執行,因此開發者無需關注這些細節,也所以代碼變得更爲簡潔。
generator是ES6裏面一個很重要的部分,目的就是解決JS異步代碼帶來的問題。generator包含不少概念,如上述的執行器,本文只是從應用場景出發,簡單解釋了generator是如何解決這些異步代碼問題的,裏面更深層次的原理未有完整覆蓋,若是須要,能夠搜索ruanyf的博客查看。