用co玩轉異步

以前我在關於Promise的文章中提到了co這個庫。在這篇文章裏,我將寫一寫本身對它的認識。git

Trust me,用了co庫,你不想用別的,來它半斤異步調用你一口能吃仨。github

可是我對Tj大神的co庫源碼談不上深刻理解。因此,若有亂講,歡迎指正。segmentfault

我這裏默認讀者對PromiseGenerator有必定的認識。數組

先安利本身寫的兩篇關於Promise的文章:瀏覽器

下面我就來談談co這個牛逼的庫。async

ES7 async/await

幹嗎,咱們不是講ES6麼,怎麼跳到ES7了?函數

由於co要作的事情,就是ES7的async/await要作的事情。
也就是說,這種解決異步的思路,已經在ECMA標準的考慮之中了。未來咱們瀏覽器的JS引擎就能夠原生實現這件事而不是經過JavaScript代碼模擬。要知道,引擎的實現和代碼的實現那是徹底兩碼事。ui

一點題外話

多一句嘴:有些同窗混淆了ECMA標準、引擎支持和代碼實現的聯繫。

這裏引用老趙在知乎裏面回答問題時說的一句話:

ES7是個標準,定義的是what to do不是how to do,爲何好多人仍是搞不清這二者的區別。

ECMAScript定義了一些JavaScript語言層面要作的事情,這是一個標準。之因此要制定這個標準,是爲了防止瀏覽器各自爲政而出現JS引擎對同一行代碼的解釋出現不一樣的狀況。

也就是說,ECMA制定標準,咱們就能夠按照這個標準來寫JavaScript代碼。寫好的JavaScript代碼由瀏覽器的JS引擎來解釋,最終變成計算機能讀懂的代碼來執行。


async/await

上代碼:

var foo = function(){

    return new Promise(resolve => {
        // 異步操做以後
        resolve('OK');
    });
}

async funtion bar(){
    
    var result = await foo();
    
    console.log(result); 
    
}

bar(); // ==> 打印'OK'

咱們注意到,這段代碼用了兩個新的關鍵字asyncawait。並且有兩件神奇的事情發生了:

  1. bar函數中包含了一個返回Promise對象的語句,並且Promise中存在異步代碼。可是這條語句接下來的語句明顯是等待Promise對象中的代碼異步執行完畢以後才執行的。(不然不會獲得異步以後的值)

  2. Promise對象resolve的值,並無在then中進行處理,而是直接做爲返回值返回到Promise對象外面了.

這就是async/await的魔法。在函數前面加上async關鍵字以後,內部的代碼會識別await關鍵字。此時假設await後面的語句返回一個Promise對象,那麼執行的代碼將會等待,直到Promise對象變爲resolve狀態。而且Promise對象中resolve的值將直接做爲await語句的返回值返回。而後再執行await語句以後的語句。

今後咱們就能夠無痛的擼異步代碼,媽媽不再用擔憂回調金字塔的出現和異步流程邏輯搞不定的狀況了!

另外一個奇妙的事情就是,率先支持這一特性的瀏覽器竟然是微軟的Edge。大概是由於C#語言早就出現async/await,而且TypeScript也支持這一特性的緣故吧。

co

咱們但願全部的瀏覽器都及早支持這一特性。可是值得欣喜的一點就是,雖然V8尚未支持,Tj大神早就利用Generator的方式實現了一個ES6版本的async/await!(膜拜臉)

co函數形式

一樣是上面的邏輯,咱們用co實現一次:

// 首先咱們須要將co引入,假設咱們使用commonJS的方式  

const co = require('co');

var foo = function(){

    return new Promise(resolve => {
        // 異步操做以後
        resolve('OK');
    });
}

co(function* (){
    
    var result = yield foo();
    
    console.log(result); 
    
}); // ==> 打印'OK'

咱們看到,co函數接收一個Generator生成器函數做爲參數。執行co函數的時候,生成器函數內部的邏輯像async函數調用時同樣被執行。不一樣之處只是這裏的await變成了yield

簡單版本的co代碼

要實現以上的邏輯,結合Generator的特性,co函數應該:

  • 在函數體內將Generator生成器函數執行並生成生成器實例(在此命名爲gen),而後經過gen.next方法的調用,不斷執行生成器函數內部的代碼。

  • 執行next方法以後,返回的Promise在生成器函數執行環境以外執行,並取出resolve值,做爲返回值做爲next方法的參數返回到Generator執行環境中。

基於以上兩點,咱們能夠大致實現一個簡化版的co,代碼以下:

const co = function(genFunc){  
    const gen = genFunc(); // 獲得生成器實例  
    
    const deal = (val) => {
        
        const res = gen.next(val); 
        
        // 這裏處理了異步邏輯,
        // 在回調中去遞歸,不斷執行next
        // 這樣就將resolve的值傳回了Generator
        res.value.then(result => deal(result));
        
    }
    
    deal(); // 第一次觸發遞歸
}

去掉括號等等,只有短短六行代碼。

more

原理性的東西大約就是這樣了。可是co作的不止這些。

  1. 以前coyield後的語句並不支持Promise對象,而是一個特殊的函數,叫作thunk。目前co兩者都支持。
    此處我並不打算重複性解釋thunk版本,由於原理性的東西實現起來是差很少的。

  2. co函數是有返回值的,也是一個Promise對象。

    • 當生成器函數內的邏輯執行完畢且沒有錯誤以後,這個Promise對象(co返回值)變爲resolve狀態,且將生成器的返回值做爲resolve出來的值。

    • 若生成器函數內返回一個Promise對象,那麼co函數返回值就是這個Promise對象。

    • 若生成器函數拋出了錯誤,那麼這個錯誤做爲reject出來的值,將Promise對象的狀態變爲reject

    這樣咱們就能夠將錯誤放進其返回值的.catch方法中統一處理。

  3. 在生成器函數內部,咱們也可使用try...catch語句獲取錯誤對象。

  4. 生成器的yield後面能夠跟一個元素值爲Promise對象的數組,這個數組內Promise對象內的異步邏輯將併發執行,並返回一個數組。(相似於Promise.all方法)

  5. 假設生成器執行以前須要從外部傳入參數,co庫提供了一個方法:

var fn = co.wrap(function* (val) {
  
     return yield Promise.resolve(val);

  });

  fn(true).then(function (val) {

  });

結束

以上是一點微小的看法。謝謝指正。

相關文章
相關標籤/搜索