經過ES6 Generator函數實現異步流程

本文翻譯自 Going Async With ES6 Generatorsjavascript

因爲我的能力知識有限,翻譯過程當中不免有紕漏和錯誤,還望指正Issuejava

ES6 Generators:完整系列

  1. The Basics Of ES6 Generators
  2. Diving Deeper With ES6 Generators
  3. Going Async With ES6 Generators
  4. Getting Concurrent With ES6 Generators

到目前爲止,你已經對ES6 generators有了初步瞭解而且可以方便的使用它,是時候準備將其運用到真實項目中提升現有代碼質量。git

Generator函數的強大在於容許你經過一些實現細節來將異步過程隱藏起來,依然使代碼保持一個單線程、同步語法的代碼風格。這樣的語法使得咱們可以很天然的方式表達咱們程序的步驟/語句流程,而不須要同時去操做一些異步的語法格式。es6

換句話說,咱們很好的對代碼的功能/關注點進行了分離:經過將使用(消費)值得地方(generator函數中的邏輯)和經過異步流程來獲取值(generator迭代器的next()方法)進行了有效的分離。github

結果就是?不只咱們的代碼具備強大的異步能力, 同時又保持了可讀性和可維護性的同步語法的代碼風格。ajax

那麼咱們怎麼實現這些功能呢?編程

最簡單的異步實現

最簡單的狀況,generator函數不須要額外的代碼來處理異步功能,由於你的程序也不須要這樣作。後端

例如,讓咱們假象你已經寫下了以下代碼:數組

function makeAjaxCall(url,cb) {
    // do some ajax fun
    // call `cb(result)` when complete
}

makeAjaxCall( "http://some.url.1", function(result1){
    var data = JSON.parse( result1 );

    makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
        var resp = JSON.parse( result2 );
        console.log( "The value you asked for: " + resp.value );
    });
} );

經過generator函數(不帶任何其餘裝飾)來實現和上面代碼相同的功能,實現代碼以下:promise

function request(url) {
    // this is where we're hiding the asynchronicity,
    // away from the main code of our generator
    // `it.next(..)` is the generator's iterator-resume
    // call
    makeAjaxCall( url, function(response){
        it.next( response );
    } );
    // Note: nothing returned here!
}

function *main() {
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
}

var it = main();
it.next(); // get it all started

讓我來解釋下上面代碼是如何工做的。

request(..)幫助函數主要對普通的makeAjaxCall(..)實用函數進行包裝,保證在在其回調函數中調用generator迭代器的next(..)方法。

在調用request(..)的過程當中,你可能已經發現函數並無顯式的返回值(換句話說,其返回undefined)。這沒有什麼大不了的,可是與本文後面的方法相比,返回值就顯得比較重要了。這兒咱們生效的yield undefined

當咱們代碼執行到yield..時(yield表達式返回undefined值),咱們僅僅在這一點暫停了咱們的generator函數而沒有作其餘任何事。等待着it.next(..)方法的執行來從新啓動該generator函數,而it.next()方法是在Ajax獲取數據結束後的回調函數(推入異步隊列等待執行)中執行的。

咱們對yield..表達式的結果作了什麼呢?咱們將其結果賦值給了變量result1。那麼咱們是怎麼將Ajax請求結果放到該yield..表達式的返回值中的呢?

由於當咱們在Ajax的回調函數中調用it.next(..)方法的時候,咱們將Ajax的返回值做爲參數傳遞給next(..)方法,這意味着該Ajax返回值傳遞到了generator函數內部,當前函數內部暫停的位置,也就是result1 = yield..語句中部。

上面的代碼真的很酷而且強大。本質上,result1 = yield request(..)做用是用來請求值,可是請求的過程幾乎徹底對咱們不可見- -或者至少在此處咱們不用怎麼擔憂它 - - 由於底層的實現使得該步驟成爲了異步操做。generator函數經過經過在yield表達式中隱藏的暫停功能以及將從新啓動generator函數的功能分離到另一個函數中,來實現了異步操做。所以在主要代碼中咱們經過一個同步的代碼風格來請求值

第二句result2 = yield result()(譯者注:做者的筆誤,應該是result2 = yield request(..))代碼,和上面的代碼工做原理幾乎無異:經過明顯的暫停和從新啓動機制來獲取到咱們請求的數據,而在generator函數內部咱們不用再爲一些異步代碼細節爲煩惱。

固然,yield的出現,也就微妙的暗示一些神奇(啊!異步)的事情可能在此處發生。和嵌套回調函數帶來的回調地獄相比,yield在語法層面上優於回調函數(甚至在API上優於promise的鏈式調用)。

須要注意上面我說的是「可能」。generator函數完成上面的工做,這自己就是一件很是強大的事情。上面的程序始終發送一個異步的Ajax請求,假如不發送異步Ajax請求呢?假若咱們改變咱們的程序來從緩存中獲取到先前(或者預先請求)Ajax請求的結果?或者從咱們的URL路由中獲取數據來馬上fulfillAjax請求,而不用真正的向後端請求數據。

咱們能夠改變咱們的request(..)函數來知足上面的需求,以下:

var cache = {};

function request(url) {
    if (cache[url]) {
        // "defer" cached response long enough for current
        // execution thread to complete
        setTimeout( function(){
            it.next( cache[url] );
        }, 0 );
    }
    else {
        makeAjaxCall( url, function(resp){
            cache[url] = resp;
            it.next( resp );
        } );
    }
}

注意:在上面的代碼中咱們使用了一個細微的技巧setTimeout(..0),當從緩存中獲取結果時來延遲代碼的執行。若是咱們不延遲而是當即執行it.next(..)方法,這將會致使錯誤的發生,由於(這就是技巧所在)此時generator函數尚未中止執行。首先咱們執行request(..)函數,而後經過yield來暫停generator函數。所以不可以在request(..)函數中當即調用it.next(..)方法,由於在此時,generator函數依然在運行(yield 尚未被調用)。可是咱們能夠在當前線程運行結束後,當即執行it.next(..)。這就是setTimeout(..0)將要完成的工做。在文章後面咱們將看到一個更加完美的解答。

如今,咱們generator函數內部主要代碼依然以下:

var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..

看到沒!?當咱們代碼從沒有緩存到上面有緩存的版本,咱們generator函數內部邏輯(咱們的控制流程)居然沒有變化。

*main()函數內部代碼依然是請求數據,暫停generator函數的執行來等待數據的返回,數據傳回後繼續執行。在咱們當前場景中,這個暫停可能相對比較長(真實的向服務器發送請求,這可能會耗時300~800ms)或者幾乎當即執行(使用setTimeout(..0)手段延遲執行)。可是咱們*main函數中的控制流程不用關心數據從何而來。

這就是從實現細節中將異步流程分離出來的強大力量。

更好的異步編程

利用上面說起的方法(回調函數),generators函數可以完成一些簡單的異步工做。可是卻至關侷限,所以咱們須要一個更增強大的異步機制來與咱們的generator函數匹配結合。完成一些更加繁重的異步流程。什麼異步機制呢?Promises

若是你依然對ES6 Promises感到困惑,我寫過關於Promise的系列文章。去閱讀一下。我會等待你回來,<滴答,滴答>。老掉牙的異步笑話了。

先前的Ajax代碼例子依然存在反轉控制的問題(啊,回調地獄)正如文章最初的嵌套回調函數例子同樣。到目前爲止,咱們應該已經明顯察覺到了上面的例子存在一些待完善的地方:

  1. 到目前爲止沒有明確的錯誤處理機制,正如咱們上一篇學習的文章,在發送Ajax請求的過程當中咱們可能檢測到錯誤(在某處),經過it.throw(..)方法將錯誤傳遞會generator函數,而後在generator函數內部經過try..catch模塊來處理該錯誤。可是,咱們在「後面」將要手動處理更多工做(更多的代碼來處理咱們的generator迭代器),若是在咱們的程序中屢次使用generators函數,這些錯誤處理代碼很難被複用。
  2. 若是makeAjaxCall(..)工具函數不受咱們控制,碰巧它屢次調用了回調函數,或者同時將成功值或者錯誤返回到generator函數中,等等。咱們的generator函數就將變得極難控制(未捕獲的錯誤,意外的返回值等)。處理、阻止上述問題的發生不少都是一些重複的工做,同時也都不是輕輕鬆鬆可以完成的。
  3. 不少時候咱們須要同時並行處理多個任務(例如兩個並行的Ajax請求)。因爲generator函數中的yield表達式執行後都會暫停函數的執行,不可以同時運行兩個或多個yield表達式,也就是說yield表達式只能按順序一個接一個的運行。所以在沒有大量手寫代碼的前提下,一個yield表達式中同時執行多個任務依然不太明朗。

正如你所見,上面的全部問題都能夠被解決,可是又有誰願意每次重複手寫這些代碼呢?咱們須要一種更增強大的模式,該模式是可信賴且高度複用的,而且可以很好的解決generator函數處理異步流程問題。

什麼模式?yield 表達式內部是promise,當這些promise被fulfill後從新啓動generator函數。

回憶上面代碼,咱們使用yield request(..),可是request(..)工具函數並無返回任何值,那麼它僅僅yield undefined嗎?

讓咱們稍微調整下上面的代碼。咱們把request(..)函數改成以promise爲基礎的函數,所以該函數返回一個promise,如今咱們經過yield表達式返回了一個真實的promise(而不是undefined)。

function request(url) {
    // Note: returning a promise now!
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } );
}

request(..)函數經過構建一個promise來監聽Ajax的完成而且resolve返回值,而且返回該promise,所以promise也可以被yield傳遞到generator函數外部,接下來呢?

咱們須要一個工具函數來控制generator函數的迭代器,該工具函數接收yield表達式傳遞出來的promise,而後在promie 狀態轉爲fulfill或者reject時,經過迭代器的next(..)方法從新啓動generator函數。如今我爲這個工具函數取名runGenerator(..):

// run (async) a generator to completion
// Note: simplified approach: no error handling here
function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

須要注意的關鍵點:

  1. 咱們自動的初始化了generator函數(建立了it迭代器),而後咱們異步運行it來完成generator函數的執行(done: true)。
  2. 咱們尋找被yield表達式傳遞出來的promise(啊,也就是執行it.next(..)方法後返回的對象中的value字段)。如此,咱們經過在promise的then(..)方法中註冊函數來監聽器完成。
  3. 若是一個非promise值被傳遞出來,咱們僅僅將該值原樣返回到generator函數內部,所以看上去當即從新啓動了generator函數。

如今咱們怎麼使用它呢?

runGenerator( function *main(){
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );

騙人!等等...上面代碼和更早的代碼幾乎徹底同樣?哈哈,generator函數再次向咱們炫耀了它的強大之處。實際上咱們建立了promise,經過yield將其傳遞出去,而後從新啓動generator函數,直到函數執行完成- - 全部被''隱藏''的實現細節!實際上並無隱藏起來,只是和咱們消費該異步流程的代碼(generator中的控制流程)隔離開來了。

經過等待yield出去的promise的完成,而後將fulfill的值經過it.next(..)方法傳遞迴函數中,result1 = yield request(..)表達式就回獲取到正如先前同樣的請求值。

可是如今咱們經過promises來管理generator代碼的異步流程部分,咱們解決了回調函數所帶來的反轉控制等問題。經過generator+promises的模式咱們「免費」解決上述所遇到的問題:

  1. 如今咱們用易用的內部錯誤處理機制。在runGenerator(..)函數中咱們並無說起,可是監聽promise的錯誤並不是難事,咱們只需經過it.throw(..)方法將promise捕獲的錯誤拋進generator函數內部,在函數內部經過try...catch模塊進行錯誤捕獲及處理。
  2. promise給咱們提供了可控性/可依賴性。不用擔憂,也不用疑惑。
  3. Promises擁有一些強大的抽象工具方法,利用這些方法能夠自動處理一些複雜的「並行」任務等。

例如,yield Prmise.all([ .. ])能夠接受一個promise數組而後「並行」執行這些任務,而後yield出去一個單獨的promise(給generator函數處理),該promise將會等待全部並行的promise都完成後才被完成,你能夠經過yield表達式的返回數組(當promise完成後)來獲取到全部並行promise的結果。數組中的結果和並行promises任務一一對應(所以其徹底忽略promise完成的順序)。

首先,讓咱們研究下錯誤處理:

// assume: `makeAjaxCall(..)` now expects an "error-first style" callback (omitted for brevity)
// assume: `runGenerator(..)` now also handles error handling (omitted for brevity)

function request(url) {
    return new Promise( function(resolve,reject){
        // pass an error-first style callback
        makeAjaxCall( url, function(err,text){
            if (err) reject( err );
            else resolve( text );
        } );
    } );
}

runGenerator( function *main(){
    try {
        var result1 = yield request( "http://some.url.1" );
    }
    catch (err) {
        console.log( "Error: " + err );
        return;
    }
    var data = JSON.parse( result1 );

    try {
        var result2 = yield request( "http://some.url.2?id=" + data.id );
    } catch (err) {
        console.log( "Error: " + err );
        return;
    }
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );

當再URL 請求發出後一個promise被reject後(或者其餘的錯誤或異常),這個promise的reject值將會映射到一個generator函數錯誤(經過runGenerator(..)內部隱式的it.throw(..)來傳遞錯誤),該錯誤將會被try..catch模塊捕獲。

如今,讓咱們看一個經過promises來管理更加錯綜複雜的異步流程的事例:

function request(url) {
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } )
    // do some post-processing on the returned text
    .then( function(text){
        // did we just get a (redirect) URL back?
        if (/^https?:\/\/.+/.test( text )) {
            // make another sub-request to the new URL
            return request( text );
        }
        // otherwise, assume text is what we expected to get back
        else {
            return text;
        }
    } );
}

runGenerator( function *main(){
    var search_terms = yield Promise.all( [
        request( "http://some.url.1" ),
        request( "http://some.url.2" ),
        request( "http://some.url.3" )
    ] );

    var search_results = yield request(
        "http://some.url.4?search=" + search_terms.join( "+" )
    );
    var resp = JSON.parse( search_results );

    console.log( "Search results: " + resp.value );
} );

Promise.all([ .. ])會構建一個新的promise來等待其內部的三個並行promise的完成,該新的promise將會被yield表達式傳遞到外部給runGenerator(..)工具函數中,runGenerator()函數監聽該新生成的promise的完成,以便從新啓動generator函數。並行的promise的返回值可能會成爲另一個URL的組成部分,而後經過yield表達式將另一個promise傳遞到外部。關於更多的promise鏈式調用,參見文章

promise能夠處理任何複雜的異步過程,你能夠經過generator函數yield出去promises(或者promise返回promise)來獲取到同步代碼的語法形式。(對於promise或者generator兩個ES6的新特性,他們的結合或許是最好的模式)

runGenerator(..): 實用函數庫

在上面咱們已經定義了runGenerator(..)工具函數來順利幫助咱們充分發揮generator+promise模式的卓越能力。咱們甚至省略了(爲了簡略起見)該工具函數的完整實現,在錯誤處理方面依然有些細微細節咱們須要處理。

可是,你不肯意實現一個你本身的runGenerator(..)是嗎?

我不這麼認爲。

許多promise/async庫都提供了上述工具函數。在此我不會一一論述,可是你一個查閱Q.spawn(..)co(..)庫,等等。

可是我會簡要的闡述我本身的庫asynquence中的runner(..)插件,相對於其餘庫,我想提供一些獨一無二的特性。若是對此感興趣並想學習更多關於asynquence的知識而不是淺嘗輒止,能夠看看之前的兩篇文章深刻asynquence

首先,asynquence提供了自動處理上面代碼片斷中的」error-first-style「回調函數的工具函數:

function request(url) {
    return ASQ( function(done){
        // pass an error-first style callback
        makeAjaxCall( url, done.errfcb );
    } );
}

是否是看起來更加好看,不是嗎!?

接下來,asynquence提供了runner(..)插件來在異步序列(異步流程)中執行generator函數,所以你能夠在runner前面的步驟傳遞信息到generator函數內,同時generator函數也能夠傳遞消息出去到下一個步驟中,同時如你所願,全部的錯誤都自動冒泡被最後的or所捕獲。

// first call `getSomeValues()` which produces a sequence/promise,
// then chain off that sequence for more async steps
getSomeValues()

// now use a generator to process the retrieved values
.runner( function*(token){
    // token.messages will be prefilled with any messages
    // from the previous step
    var value1 = token.messages[0];
    var value2 = token.messages[1];
    var value3 = token.messages[2];

    // make all 3 Ajax requests in parallel, wait for
    // all of them to finish (in whatever order)
    // Note: `ASQ().all(..)` is like `Promise.all(..)`
    var msgs = yield ASQ().all(
        request( "http://some.url.1?v=" + value1 ),
        request( "http://some.url.2?v=" + value2 ),
        request( "http://some.url.3?v=" + value3 )
    );

    // send this message onto the next step
    yield (msgs[0] + msgs[1] + msgs[2]);
} )

// now, send the final result of previous generator
// off to another request
.seq( function(msg){
    return request( "http://some.url.4?msg=" + msg );
} )

// now we're finally all done!
.val( function(result){
    console.log( result ); // success, all done!
} )

// or, we had some error!
.or( function(err) {
    console.log( "Error: " + err );
} );

asyquence的runner(..)工具接受上一步序列傳遞下來的值(也有可能沒有值)來啓動generator函數,能夠經過token.messages數組來獲取到傳入的值。

而後,和上面咱們所描述的runGenerator(..)工具函數相似,runner(..)也會監聽yield一個promise或者yield一個asynquence序列(在本例中,是指經過ASQ().all()方法生成的」並行」任務),而後等待promise或者asynquence序列的完成後從新啓動generator函數。

當generator函數執行完成後,最後經過yield表達式傳遞的值將做爲參數傳遞到下一個序列步驟中。

最後,若是在某個序列步驟中出現錯誤,甚至在generator內部,錯誤都會冒泡到被註冊的or(..)方法中進行錯誤處理。

asynquence經過儘量簡單的方式來混合匹配promises和generator。你能夠自由的在以promise爲基礎的序列流程後面接generator控制流程,正如上面代碼。

ES7 async

在ES7的時間軸上有一個提案,而且有極大可能被接受,該提案將在JavaScript中添加另一個函數類型:async函數,該函數至關於用相似於runGenerator(..)(或者asynquence的runner(..))工具函數在generator函數外部包裝一下,來使得其自動執行。經過async函數,你能夠把promises傳遞到外部而後async函數在promises狀態變爲fulfill時自動從新啓動直到函數執行完成。(甚至不須要複雜的迭代器參與)

async函數大概形式以下:

async function main() {
    var result1 = await request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = await request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
}

main();

正如你所見,async 函數能夠想普通函數同樣被調用(如main()),而不須要包裝函數如runGenerator(..)或者ASQ().runner(..)的幫助。同時,函數內部再也不使用yield,而是使用await(另一個JavaScript關鍵字)關鍵字來告訴async 函數等待當前promise獲得返回值後繼續執行。

基本上,async函數擁有經過一些包裝庫調用generator函數的大部分功能,同時關鍵是其被原生語法所支持

是否是很酷!?

同時,像asynquence這樣的工具集使得咱們可以輕易的且充分利用generator函數完成異步工做。

總結

簡單地說:經過把promise和generator函數兩個世界組合起來成爲generator + yield promise(s)模式,該模式具備強大的能力及同步語法形式的異步表達能力。經過一些簡單包裝的工具(不少庫已經提供了這些工具),咱們可讓generator函數自動執行完成,而且提供了健全和同步語法形式的錯誤處理機制。

同時在ES7+的未來,咱們也許將迎來async function函數,async 函數將不須要上面那些工具庫就可以解決上面遇到的那些問題(至少對於基礎問題是可行的)!

JavaScript的異步處理機制的將來是光明的,並且會愈來愈光明!我要帶墨鏡了。(譯者注:這兒是做者幽默的說法)

可是,咱們並無在這兒就結束本系列文章,這兒還有最後一個方面咱們想要研究:

假若你想要將兩個或多個generator函數結合在一塊兒,讓他們獨立平行的運行,而且在它們執行的過程當中來來回回得傳遞信息?這必定會成爲一個至關強大的特性,難道不是嗎?這一模式被稱做「CSP」(communicating sequential processes)。咱們將在下面一篇文章中解鎖CSP的能力。敬請密切關注。

相關文章
相關標籤/搜索