異步流程控制:7 行代碼學會 co 模塊

廣告招人:阿里巴巴招前端,在這裏你能夠享受大公司的福利和技術體系,也有小團隊的挑戰和成長空間。
聯繫: qingguang.meiqg at alibaba-inc.comjavascript

首先請原諒個人標題黨(●—●),tj 大神的 co 模塊源碼200多行,顯然不是我等屌絲能隨便幾行代碼就能重寫的。只是當今你們都喜歡《7天學會xx語言》之類的速效仙丹,因而我也弄個相似的名字《7行代碼學會co模塊》來博眼球。html

爲了不被拖出去彈小JJ,仍是先放出所謂的 7 行代碼給你們壓壓驚:前端

function co(gen) {
    var it = gen();
    var ret = it.next();
    ret.value.then(function(res) {
        it.next(res);
    });
}

萬惡的回調

對前端工程師來講,異步回調是再熟悉不過了,瀏覽器中的各類交互邏輯都是經過事件回調實現的,前端邏輯愈來愈複雜,致使回調函數愈來愈多,同時 nodejs 的流行也讓 javascript 在後端的複雜場景中獲得應用,在 nodejs 代碼中更是常常看到層層嵌套。java

如下是一個典型的異步場景:先經過異步請求獲取頁面數據,而後根據頁面數據請求用戶信息,最後根據用戶信息請求用戶的產品列表。過多的回調函數嵌套,使得程序難以維護,發展成萬惡的回調node

$.get('/api/data', function(data) {
    console.log(data);
    $.get('/api/user', function(user) {
        console.log(user);
        $.get('/api/products', function(products) {
            console.log(products)
        });
    });
});

異步流程控制

  • 最原始異步流程的寫法,就是相似上面例子裏的回調函數嵌套法,用過的人都知道,那叫一個酸爽。git

  • 後來出現了 Promise ,它極大提升了代碼的可維護性,消除了萬惡的回調嵌套問題,而且如今已經成爲 ES6 標準的一部分。github

$.get('/api/data')
.then(function(data) {
    console.log(data);
    return $.get('/api/user');
})
.then(function(user) {
    console.log(user);
    return $.get('/api/products');
})
.then(function(products) {
    console.log(products);
});
  • 以後在 nodejs 圈出現了 co 模塊,它基於 ES6 的 generator 和 yield ,讓咱們能用同步的形式編寫異步代碼。算法

co(function *() {
    var data = yield $.get('/api/data');
    console.log(data);
    var user = yield $.get('/api/user');
    console.log(user);
    var products = yield $.get('/api/products');
    console.log(products);
});
  • 以上的 Promise 和 generator 最初創造它的本意都不是爲了解決異步流程控制。其中 Promise 是一種編程思想,用於「當xx數據準備完畢,then執行xx動做」這樣的場景,不僅是異步,同步代碼也能夠用 Promise。而 generator 在 ES6 中是迭代器生成器,被 TJ 創造性的拿來作異步流程控制了。真正的異步解決方案請你們期待 ES7 的 async 吧!本文如下主要介紹 co 模塊。編程

co 模塊

上文已經簡單介紹了co 模塊是能讓咱們以同步的形式編寫異步代碼的 nodejs 模塊,主要得益於 ES6 的 generator。nodejs >= 0.11 版本能夠加 --harmony 參數來體驗 ES6 的 generator 特性,iojs 則已經默認開啓了 generator 的支持。後端

要了解 co ,就不得不先簡單瞭解下 ES6 的 generator 和 iterator。

Iterator

Iterator 迭代器是一個對象,知道如何從一個集合一次取出一項,而跟蹤它的當前序列所在的位置,它提供了一個next()方法返回序列中的下一個項目。

var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
var pair = it.next(); 
console.log(pair); // ["name", "JavaScript"]
pair = it.next(); 
console.log(pair); // ["birthYear", 1995]
pair = it.next(); // A StopIteration exception is thrown

乍一看好像沒什麼奇特的,不就是一步步的取對象中的 key 和 value 嗎,for ... in也能作到,可是把它跟 generator 結合起來就大有用途了。

Generator

Generator 生成器容許你經過寫一個能夠保存本身狀態的的簡單函數來定義一個迭代算法。
Generator 是一種能夠中止並在以後從新進入的函數。生成器的環境(綁定的變量)會在每次執行後被保存,下次進入時可繼續使用。generator 字面上是「生成器」的意思,在 ES6 裏是迭代器生成器,用於生成一個迭代器對象。

function *gen() {
    yield 'hello';
    yield 'world';
    return true;
}

以上代碼定義了一個簡單的 generator,看起來就像一個普通的函數,區別是function關鍵字後面有個*號,函數體內可使用yield語句進行流程控制。

var iter = gen();
var a = iter.next();
console.log(a); // {value:'hello', done:false}
var b = iter.next();
console.log(b); // {value:'world', done:false}
var c = iter.next();
console.log(c); // {value:true, done:true}

當執行gen()的時候,並不執行 generator 函數體,而是返回一個迭代器。迭代器具備next()方法,每次調用 next() 方法,函數就執行到yield語句的地方。next() 方法返回一個對象,其中value屬性表示 yield 關鍵詞後面表達式的值,done 屬性表示是否遍歷結束。generator 生成器經過nextyield的配合實現流程控制,上面的代碼執行了三次 next() ,generator 函數體才執行完畢。

co 模塊思路

從上面的例子能夠看出,generator 函數體能夠停在 yield 語句處,直到下一次執行 next()。co 模塊的思路就是利用 generator 的這個特性,將異步操做跟在 yield 後面,當異步操做完成並返回結果後,再觸發下一次 next() 。固然,跟在 yield 後面的異步操做須要遵循必定的規範 thunks 和 promises。

yieldables

The yieldable objects currently supported are:

  • promises

  • thunks (functions)

  • array (parallel execution)

  • objects (parallel execution)

  • generators (delegation)

  • generator functions (delegation)

7行代碼

再看看文章開頭的7行代碼:

function co(gen) {
    var it = gen();
    var ret = it.next();
    ret.value.then(function(res) {
        it.next(res);
    });
}

首先生成一個迭代器,而後執行一遍 next(),獲得的 value 是一個 Promise 對象,Promise.then() 裏面再執行 next()。固然這只是一個原理性的演示,不少錯誤處理和循環調用 next() 的邏輯都沒有寫出來。

下面作個簡單對比:
傳統方式,sayhello是一個異步函數,執行helloworld會先輸出"world"再輸出"hello"

function sayhello() {
    return Promise.resolve('hello').then(function(hello) {
        console.log(hello);
    });
}
function helloworld() {
    sayhello();
    console.log('world');
}
helloworld();

輸出

> "world"
> "hello"

co 的方式,會先輸出"hello"再輸出"world"

function co(gen) {
    var it = gen();
    var ret = it.next();
    ret.value.then(function(res) {
        it.next(res);
    });
}
function sayhello() {
    return Promise.resolve('hello').then(function(hello) {
        console.log(hello);
    });
}
co(function *helloworld() {
    yield sayhello();
    console.log('world');
});

輸出

> "hello"
> "world"

消除回調金字塔

假設sayhello/sayworld/saybye是三個異步函數,用真正的 co 模塊就能夠這麼寫:

var co = require('co');
co(function *() {
    yield sayhello();
    yield sayworld();
    yield saybye();
});

輸出

> "hello"
> "world"
> "bye"

參考

《es7-async》 https://github.com/jaydson/es7-async
《Generator 函數的含義與用法》 http://www.ruanyifeng.com/blog/2015/04/generator.html
《Iterator》 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Iterator

相關文章
相關標籤/搜索