/** * co & yield 培訓例程. TJ's co test, 參考和下載: https://github.com/visionmedia/co * 運行環境: 安裝 nodejs v0.11.2以上版本, 推薦IDE: Jetbrains WebStorm 8.0 * 依賴的包: 請先 npm install -g co thunkify request //全局化安裝, * 再到本js所在目錄下 npm link co thunkify request 引用這些全局安裝模塊 * 執行: node --harmony co.js ,必須帶上--harmony參數才支持 ES6特性 * WebStorm 調試: 在Run/Debug Configuration -> Node parameters 中添加 --harmony * 學習方法: 在WebStorm的Debugger中, 設置斷點, 單步執行. 這樣就能清楚看到yield執行順序 * Created with WebStorm. * User: JimmyCHEN(290958374@qq.com) * Created: 2014/1/24 8:17 * Modified: 2014/2/2 11:20 * 中文說明編寫: 2014/7/7 13:40 */ var assert = require('assert'); var co = require('co'); var fs = require('fs'); // co 是利用Generator和yield原理編寫的一個包, 具備運行相似於「協程」的功能。利用co 和 yield 編寫和執行異步操做, 能夠徹底擺脫nodejs的回調深坑, // 大幅提升代碼溫馨度和團隊生產力. 所以,co是深受回調深坑困然的nodejs碼農們的不二選擇! // 對於現有的傳統回調式的異步函數(如fs.readFile, http.request等),須要將其封裝(轉換)成爲能夠yield的函數(該函數稱爲「轉換器」), 供yield調用 // 一個轉換器的格式通常是這樣的: function read(file, encoding) { //這個轉換器只返回一個調用fs.readFile的符合yield要求的函數, 而不是執行fs.readFile encoding = encoding || 'utf-8'; return function(callback) { //轉換器返回的是一個函數, 其參數必須是被異步函數(fs.readFile)回調的callback. 轉換器自己不執行fs.readFile fs.readFile(file, encoding, callback);//callback的參數是 (err, result), 這裏的 `result` 最終將被做爲yield read()的返回值. } } //*上述 function 等價於 var read = thunkify(fs.readFile); //若是採用現成的thunkify模塊的話, 就不用本身寫上面那樣的轉換器函數了, 省事! (留做後文介紹) //API-1: co(fn) //異步函數通過上述轉換之後, 就能夠在co裏用yield去執行. 執行yield化的異步函數, 就如執行同步函數同樣簡單溫馨, 再也沒有回調深坑的煩惱. 我和個人小夥伴都驚呆了! 請看大屏幕: co(function* () { //被 co 的函數必須是個生成器(generator), 用 function* 定義 var a = yield read('.dbshell'); //異步讀文件1, 內容返回給 a. 在內容沒讀到前, yield將退出函數的執行, 下一句(var b =...)不會被執行. // 直到 a 獲得返回內容, 才接着執行下一句. 實際上, 返回值是被fs.readFile中的callback返回, 而後觸發(喚醒)yield 並賦值給a // *請根據實際目錄內容修改文件名稱, 下同 var b = yield read('config.json'); //異步讀文件2, 內容返回給 b var c = yield read('note.txt'); //異步讀文件3, 內容返回給 c //三個 read 被順序依次執行, 最後獲得三個值 a,b,c console.log([a, b, c]); // => [5171, 2090, 1477] })(); //=========================================================================================================// //co & yield化後的錯誤處理: 用try..catch co(function* () { //被 co 的函數必須是個生成器(generator), 用 function* 定義 var a = yield read('.dbshell'); //異步讀文件1, 內容返回給 a var b = yield read('config.json'); //異步讀文件2, 內容返回給 b var c = yield read('note.txt'); //異步讀文件2, 內容返回給 c //上面三個 read 被順序依次執行, 最後獲得三個值 a,b,c //錯誤處理 try { var d = yield read('not exists.txt'); //文件不存在, 用於產生錯誤 } catch (e) { console.log(e); // => 輸出: 文件不存在的錯誤 } console.log([a, b, c]); // => [5171, 2090, 1477] })(); //=========================================================================================================// //你也能夠 yield 執行一個 generator 對象, 以支持嵌套: //用於讀取文件大小的轉換器 function size(file) { return function(callback){ fs.stat(file,function(err, result){ if(err) return callback(err); callback(null, result.size); //異步執行結束後將返回result.size給yield }); } } function* foo(){ var a = yield size('.dbshell'); var b = yield size('config.json'); var c = yield size('note.txt'); //debugger; return [a, b, c]; // => [ 994, 187, 16212 ] } function* bar(){ var a = yield size('test/co.js'); var b = yield size('test/test.js'); var c = yield size('test/class.js'); //debugger; return [a, b, c]; // => [ 9023, 2090, 1477 ] } co(function*(){ var results = yield [foo(), bar()];//yield 一個數組對象. //也能夠按下面的例子寫成 [foo, bar] //debugger; console.log(results);// => [ [ 994, 187, 16212 ], [ 9023, 2090, 1477 ] ] })(); //=========================================================================================================// //對於那麼多傳統回調式異步函數, 都去手工編寫一個轉換函數是否是太麻煩了? 別擔憂, 咱們有tunkify. 顧名思義, 它就是現成的轉換器函數: var thunkify = require('thunkify'); var request = require('request'); //用於http.request的包 //轉換器:thunkify, 將傳統的回調式的函數,轉換成可yield的函數 //轉換request.get函數 var get = thunkify(request.get); //request.get = function (uri, options, callback), where callback = function (err, response, body) function* results(){ var a = get('http://www.baidu.com');//這裏只是獲得函數,不會發起請求 var b = get('http://www.163.com'); //用yield 執行 get纔會返回http.get的callback裏的返回內容 var c = get('http://www.126.com'); return yield [a, b, c];//這裏才真正發起http.get請求, 並且是同時發起3次併發請求. 用yield [數組], 將「併發執行」數組中的全部操做 } co(function*(){//再次強調, co裏的函數必須是生成器! var a = yield results; // 每條yield []將發起3次併發執行 var b = yield results; //兩條 yield 則是順序執行 console.log(a, b); // => [三個網頁內容] , [三個網頁內容] var c = yield [results, results];//這裏將產生6個併發http操做! console.log(c);// => 輸出: [[三個網頁內容],[三個網頁內容]] })(); //=========================================================================================================// //併發 yield: 逗號表達式中的全部yield將被併發執行. co(function*(){ var a = size('test/test.js'); var b = size('test/class.js'); var c = size('test/class2.js'); //debugger; return [yield a, yield b, yield c]; })(); //=========================================================================================================// //下面演示如何取得多個返回值(只要將第2個值附加到第1值上便可) , 例: callback(err, response, body) 中的body值 var get2 = function(uri) { return function(callback){ request.get(uri, function(err, response, body) { if(err) return callback(err); if(typeof body == 'object') {//如果json,則附加到response上用於yield返回值 response.bodyObject = body;//*這裏只是爲了演示須要! 實際上若request.get參數裏指定json:true,則response.body 自己就是json了,沒必要另行附加body } callback(null, response); //返回response }); } } function* results2(){ var a = get2({url:'http://market.huobi.com/staticmarket/ticker_ltc_json.js',json:false});//這裏只獲得函數,不會發起請求 var b = get2({url:'http://market.huobi.com/staticmarket/ticker_btc_json.js',json:true});//yield get纔會返回callback裏的response var c = get2({url:'http://market.huobi.com/staticmarket/depth_ltc_json.js',json:true});//callback(err, response,body)中的body將做爲response.bodyObject返回 return yield [a, b, c];//這裏才發起請求, 並且是3次併發請求(yield數組對象) } co(function*(){ // 3 concurrent requests at a time var a = yield results2; //3個併發get var b = yield results2; //3個併發get console.log(a, b); //輸出:[三個網頁內容], [三個網頁內容] assert.equal(typeof a[0].body, 'string', 'Error: a[0].body is not a string');//equal:(value, expected, errorMessage) assert.equal(typeof a[0].bodyObject, 'undefined', 'Error: a[0].bodyObject is not undefined'); assert.equal(typeof b[1].bodyObject, 'object', 'Error: b[1].bodyObject is not an Object'); var c = yield [results2, results2];//6 個併發get! console.log(c);//輸出:[[三個網頁內容], [三個網頁內容]] })(); //=========================================================================================================// //數組: yield 數組 //yield執行數組, 數組裏的全部步驟都是併發操做. 最後全部返回值組成一個數組. co(function* yieldArray(){ //!再次強調: co裏必須是個generator, 用 function* 定義 var a = size('test/co.js'); var b = size('test/test.js'); var c = size('test/class.js'); //debugger; var res = yield[a, b, c]; console.log(res); // => [5171, 2090, 1477] })(); //嵌套數組: 數組裏面還能夠嵌套數組. 下面代碼, 執行時至關於併發執行6個操做: co(function *() { var a = [ get('http://sina.com'), get('http://baidu.com'), get('http://163.com') ]; var b = [ get('http://sina.com'), get('http://yahoo.com'), get('http://ign.com') ]; console.log(yield [a, b]); // 數組裏嵌套數組, 將併發執行6個http.get! })(); //對象: yield object, 相似數組, 不過還支持遞歸 co(function* yieldObject(){ var user = yield { //yield在 object 外面, 則 object 內部的轉換器將併發執行 name: { first: get('http://www.iciba.com/first'), //這裏 object 裏層只是轉換器, 沒有yield last: get('http://www.iciba.com/last') // 所以這兩個get也是併發執行 } }; //最終合成 user object = {name: {first: "網頁內容1", last: "網頁內容2"}} console.log(user); // => {"name": {"first": "網頁內容1", "last": "網頁內容2"}} })(); //若是對象內屬性值裏用yield, 則每一個yield是順序執行(非併發): co(function *(){ var user = { name: { first: yield get('http://www.iciba.com/first'), //這裏 object 裏的屬性值經過 yield 獲取 last: yield get('http://www.iciba.com/last') // 所以這兩個get 是順序執行(非併發) } }; console.log(user); // => {"name": {"first": "網頁內容1", "last": "網頁內容2"}} })(); //性能: TJ的測試結果 //在我本機上順序執行 30,000 次 stat() 操做平均耗時 570ms, 而用 co() 執行一樣次數的 stat() 平均耗時 610ms, //也就是說, yield 帶來的損耗幾乎能夠忽略不計. //類(Class)裏的 generator & yield //定義類的構造函數 function Class () { this.name = 'Class'; } //定義類的類的成員函數 generator Class.prototype.generator = function*(){ var user = yield { //yield 一個對象 name: { first: get('http://www.iciba.com/first'), last: get('http://www.iciba.com/last') } }; console.log(user); // => {"name": {"first": "網頁內容1", "last": "網頁內容2"}} console.log('Class.run() end.') }; //定義一個函數用於執行 Class.prototype.run = function() { co(this.generator)(); //用 co() 去執行 } //建立類的實例 var obj = new Class(); //運行 obj.run(); // => {"name": {"first": "網頁內容1", "last": "網頁內容2"}}