2015年7月8日 星期三

快樂玩 ES6 Generator,從 co 起手式開始

Standard

自從 Node.js 0.12 版和 io.js 之後,大量的開發者開始了各自的 ECMAScript 6 大冒險,許多人對 Generator 的使用仍跌跌撞撞,對於這種看似「同步(Synchronous)」的「異步(Asynchronous)」機制,有許多人腦袋遲遲無法轉過來。雖然在小弟的書(參閱連結:新書報到!Node.js 模組參考手冊!)已經有清楚的說明 Generator 使用方法,但就許多讀者回函來看,對於 JavaScript 越是熟悉的人,越無法直觀理解 Generator 的思維,甚至是老是抓不准使用的時機點。

尤其是過去我們已經有 Promise、async、Q 和 bluebird 等處理非同步程式流程的模組和工具,很多人就是覺得沒有使用 Generator 的必要。不過,如果你會使用 co 模組,你會突然發現若是將過去的流程機制與 Generator 相搭配,程式開發將變得更為流暢。

若想要安裝 co 模組,可以直接以 NPM 下載:
npm install co

本文接下來會說明一些 co 的基本使用,破除一些 Generator 難以使用的地方,讓開發者們更容易開始 Generator 的旅程。

讓原生 Generator 更好用

這是很多人不喜歡使用 Generator 的主因,以往為了使用 Generator,我們還要先建立一個 Generator 的函數,然後不時的去處理 Generator 所返回的資訊,一遍又一遍進入 Generator 之中。因此,無論 Generator 再好用,這些麻煩的動作也完全抵銷他的優勢,大多數人還不如回到舊的流程控制方法,以免徒增自己的麻煩。

而如果使用 co,可以直接將 Generator 函數當作「立即函數使用」,其餘的部份我們可以不需要擔心:
var co = require('co');

co(function *() {
    console.log('Inside'); 
}) 

再也不用煩惱 yield!

以前光是 Promise,就已經讓很多人詬病,覺得每次使用 Promise 都要花許多時間對函數進行包裝,而 Generator 也有類似的問題,若是要使用 yield,更是一件大工程。於是,co 幫開發者做了些設計,讓 Generator 可以直接利用 yield 去跑 Promise 類型的函數、陣列等物件,因為是 yield,其執行模式是「異步」,不會阻塞事件引擎。

co 支援 Trunks,也就是回傳一個簡單函數進行異步工作,範例如下:
var co = require('co');

function updateInfo() {

    // 返回一個將被執行的函數 
    return function(done) { 

        // 可以執行各種異步工作
        setTimeout(function() {

            // 完成工作後執行 callback,參數一為錯誤訊息,參數二為回傳值
            done(null, 'Done'); 
        }, 1000);
    };
}

co(function *() {
    // 執行並等待回傳值 
    var ret = yield updateInfo();

    // 一秒後收到回傳值並印出來
    console.log(ret); 
});

支援 Promise 的 yield

如果你熟悉 Promise,這是個好消息, co 讓 yield 可以直接吃 Promise:
co(function *() {
    var ret = yield Promise.resolve(true);
});

直接使用 yield 跑一整個陣列的工作

前面提到 co 讓 yield 也支援陣列,所以我們可以準備一系列的工作,放在陣列中讓 yield 去處理:
co(function *() {
    var a = Promise.resolve(true);
    var b = Promise.resolve(true);
    var c = Promise.resolve(true); 
    var ret = yield [ a, b, c ];
});

異步處理一個陣列裡的所有元素

這大概是以往 JavaScript 最頭痛的工作,後來有了 async 這類模組後,才比較容易處理,不然光使用 forEach 或是 for 迴圈來做,碰到較大的陣列,往往使程式阻塞卡死。如果使用 co 前面的起手式,我們可以試著這樣做(如果不使用 Promise):
var arr = [ 1, 2, 3, 4, 5 ];

function handle(el) {

    return function() {
        // 處理傳入值 
        console.log(el); 
    };
} 

co(function *() {

    // 在 Generator 裡,使用 yield 不會使 JavaScript 事件引擎阻塞,
    // 但因為程式流程會等 yield 結束,for 迴圈也不會造成阻塞
    for (var index in arr) {
        var el = arr[index];
         yield handle(el);
    }
});

後記

經過 co 包裝後的 Generator 非常好用,無論你過去習慣用哪一種流程控制的方式,都可以很好的轉換並整合過來。試試看吧!:-)