ES6 Generators的異步應用

  ES6 Generators系列:

  1. ES6 Generators基本概念
  2. 深刻研究ES6 Generators
  3. ES6 Generators的異步應用
  4. ES6 Generators併發

  經過前面兩篇文章,咱們已經對ES6 generators有了一些初步的瞭解,是時候來看看如何在實際應用中發揮它的做用了。html

  Generators最主要的特色就是單線程執行,同步風格的代碼編寫,同時又容許你將代碼的異步特性隱藏在程序的實現細節中。這使得咱們能夠用很是天然的方式來表達程序或代碼的流程,而不用同時還要兼顧如何編寫異步代碼。git

  也就是說,經過generator函數,咱們將程序具體的實現細節從異步代碼中抽離出來(經過next(..)來遍歷generator函數),從而很好地實現了功能和關注點的分離。github

  其結果就是代碼易於閱讀和維護,在編寫上具備同步風格,但卻支持異步特性。那如何才能作到這一點呢?ajax

 

最簡單的異步

  一個最簡單的例子,generator函數內部不須要任何異步執行代碼便可完成整個異步過程的調用。編程

  假設你有下面這段代碼:設計模式

function makeAjaxCall(url,cb) {
    // ajax請求
    // 完成時調用cb(result)
}

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函數來實現上面代碼的邏輯:數組

function request(url) {
    // 這裏的異步調用被隱藏起來了,
    // 經過it.next(..)方法對generator函數進行迭代,
    // 從而實現了異步調用與main方法之間的分離
    makeAjaxCall( url, function(response){
        it.next( response );
    } );
    // 注意:這裏沒有return語句!
}

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(); // 開始

  解釋一下上面的代碼是如何運行的。promise

  方法request(..)是對makeAjaxCall(..)的封裝,確保回調可以調用generator函數的next(..)方法。請注意request(..)方法中沒有return語句(或者說返回了一個undefined值),後面咱們會講到爲何要這麼作。緩存

  Main函數的第一行,因爲request(..)方法沒有任何返回值,因此這裏的yield request(..)表達式不會接收任何值進行計算,僅僅暫停了main函數的運行,直到makeAjaxCall(..)在ajax的回調中執行it.next(..)方法,而後恢復main函數的運行。那這裏yield表達式的結果究竟是什麼呢?咱們將什麼賦值給了變量result1?在Ajax的回調中,it.next(..)方法將Ajax請求的返回值傳入,這個值會被yield表達式返回給變量result1服務器

  是否是很酷!這裏,result1 = yield request(..)事實上就是爲了獲得ajax的返回結果,只不過這種寫法將回調隱藏起來了,咱們徹底不用擔憂,由於其中具體的執行步驟就是異步調用。經過yield表達式的暫停功能,咱們將程序的異步調用隱藏起來,而後在另外一個函數(ajax的回調)中恢復對generator函數的運行,整個過程使得咱們的main函數的代碼看起來就像是在同步執行同樣

  語句result2 = yield result(..)的執行過程與上面同樣。代碼執行過程當中,有關generator函數的暫停和恢復徹底是透明的,程序最終將咱們想要的結果返回回來,而全部的這些都不須要咱們將注意力放在異步代碼的編寫上。

  固然,代碼中少不了yield關鍵字,這裏暗示着可能會有一個異步調用。不過這和地獄般的嵌套回調(或者promise鏈)比起來,代碼看起來要清晰不少。

  注意上面我說的yield關鍵字的地方是「可能」會出現一個異步調用,而不是必定會出現。在上面的例子中,程序每次都會去調用一個Ajax的異步請求,但若是咱們修改了程序,將以前Ajax響應的結果緩存起來,狀況會怎樣呢?又或者咱們在程序的URL請求路由中加入某些邏輯判斷,使其當即就返回Ajax請求的結果,而不是真正地去請求服務器,狀況又會怎樣呢?

  咱們將上面的代碼改爲下面這個版本:

var cache = {};

function request(url) {
    if (cache[url]) {
        // 延遲返回緩存中的數據,以保證當前執行線程運行完成
        setTimeout( function(){
            it.next( cache[url] );
        }, 0 );
    }
    else {
        makeAjaxCall( url, function(resp){
            cache[url] = resp;
            it.next( resp );
        } );
    }
}

  注意上面代碼中的setTimeout(..)語句,它會延遲返回緩存中的數據。若是咱們直接調用it.next(..)程序會報錯,這是由於generator函數目前還不是處於暫停狀態。主函數在調用完request(..)以後,generator函數纔會處於暫停狀態。因此,咱們不能在request(..)函數內部當即執行it.next(..),由於此時的generator函數仍然處於運行中(即yield表達式尚未被處理)。不過咱們能夠稍後再調用it.next(..)setTimeout(..)語句將會在當前執行線程完成後當即執行,也就是在request(..)方法執行完後再執行,這正是咱們想要的。下面咱們會有更好的解決方案。

  如今,咱們的main函數的代碼依然是這樣:

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

  瞧!咱們的程序從不帶緩存的版本改爲了帶緩存的版本,可是main函數卻不用作任何修改。*main()函數依然只是請求一個值,而後暫停運行,直到請求返回一個結果,而後再繼續運行。當前程序中,暫停的時間可能會比較長(實際Ajax請求大概會在300-800ms之間),但也多是0(使用setTimeout(..0)延遲的狀況)。不管是哪一種狀況,咱們的主流程是不變的。

  這就是將異步過程抽象爲實現細節的真正力量!

 

改進的異步

  以上方法僅適用於一些簡單異步處理的generator函數,很快你就會發如今大多數實際應用中根本不夠用,因此咱們須要一個更強大的異步處理機制來匹配generator函數,使其可以發揮更大的做用。這個處理機制是什麼呢?答案就是promises. 若是你對ES6 Promises還不瞭解,能夠看看這裏的一篇文章: http://blog.getify.com/promises-part-1/

  在前面的Ajax示例代碼中,無一例外都會遇到嵌套回調的問題(咱們稱之爲回調地獄)。到目前爲止咱們還有一些東西沒有考慮到:

  1. 有關錯誤處理。在前一篇文章中咱們已經介紹過如何在generator函數中處理錯誤,咱們能夠在Ajax的回調中判斷是否出錯,並經過it.throw(..)方法將錯誤傳遞給generator函數,而後在generator函數中使用try..catch語句來處理它。但這無疑會帶來許多工做量,並且若是程序中有不少generator函數的話,代碼也不容易重用。
  2. 若是makeAjaxCall(..)函數不在咱們的控制範圍內,而且它會屢次調用回調,或者同時返回success和error等等,那麼咱們的generator函數將會陷於混亂(未處理的異常,返回意外的值等)。要解決這些問題,你可能須要作不少額外的工做,這顯然很不方便。
  3. 一般咱們須要「並行」來處理多個任務(例如同時發起兩個Ajax請求),因爲generator函數的yield只容許單個暫停,所以兩個或多個yield不能同時運行,它們必須按順序一個一個地運行。因此,在不編寫大量額外代碼的前提下,很難在generator函數的單個yield中同時處理多個任務。

  上面的這些問題都是能夠解決的,可是誰都不想每次都面對這些問題而後從頭至尾地解決一遍。咱們須要一個功能強大的設計模式,可以做爲一個可靠的而且能夠重用的解決方案,應用到咱們的generator函數的異步編程中。這種模式要可以返回一個promises,而且在完成以後恢復generator函數的運行。

  回想一下上面代碼中的yield request(..)表達式,函數request(..)沒有任何返回值,但實際上這裏咱們是否是能夠理解爲yield返回了一個undefined呢?

  咱們將request(..)函數改爲基於promises的,這樣它會返回一個promise,因此yield表達式的計算結果也是一個promise而不是undefined

function request(url) {
    // 注意:如今返回的是一個promise!
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } );
}

  如今,request(..)函數會構造一個Promise對象,並在Ajax調用完成以後進行解析,而後返回一個promise給yield表達式。而後呢?咱們須要一個函數來控制generator函數的迭代,這個函數會接收全部的這些yield promises而後恢復generator函數的運行(經過next(..)方法)。咱們假設這個函數叫runGenerator(..)

// 異步調用一個generator函數直到完成
// 注意:這是最簡單的狀況,不包含任何錯誤處理
function runGenerator(g) {
    var it = g(), ret;

    // 異步迭代給定的generator函數
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // 簡單測試返回值是不是一個promise
            if ("then" in ret.value) {
                // 等待promise返回
                ret.value.then( iterate );
            }
            // 當即執行
            else {
                // 避免同步遞歸調用
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

  幾個關鍵的點:

  1. 程序會自動初始化generator函數(建立迭代器it),而後異步運行直到完成(done:true)。
  2. 查看yield是否返回一個promise(經過it.next(..)返回值中的value屬性來查看),若是是,則等待promise中的then(..)方法執行完。
  3. 任何當即執行的代碼(非promise類型)將會直接返回結果給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函數同樣嗎?是的。不過在這個版本中,咱們建立了promises並返回給yield,等promise完成以後恢復generator函數繼續運行。全部這些操做都「隱藏」在實現細節中!不過不是真正的隱藏,咱們只是將它從消費代碼(這裏指的是咱們的generator函數中的流程控制)中分離出去而已。

  Yield接受一個promise,而後等待它完成以後返回最終的結果給it.next(..)。經過這種方式,語句result1 = yield request(..)可以獲得和以前同樣的結果。

  如今咱們使用promises來管理generator函數中異步調用部分的代碼,從而解決了在回調中所遇到的各類問題:

  1. 擁有內置的錯誤處理機制。雖然咱們並無在runGenerator(..)函數中顯示它,可是從promise監聽錯誤並不是難事,一旦監聽到錯誤,咱們能夠經過it.throw(..)將錯誤拋出,而後經過try..catch語句捕獲和處理這些錯誤。
  2. 咱們經過promises來控制全部的流程。這一點毋庸置疑。
  3. 在自動處理各類複雜的「並行」任務方面,promises擁有十分強大的抽象能力。例如,yield Promise.all([..])接收一個「並行」任務的promises數組,而後yield一個單個的promise(返回給generator函數處理),這個單個的promise會等待數組中全部的promises所有處理完以後纔會開始,但這些promises的執行順序沒法保證。當全部的promises執行完後,yield表達式會接收到另一個數組,數組中的值是每一個promise返回的結果,按照promise被請求的順序依次排列。

  首先咱們來看一下錯誤處理:

// 假設:`makeAjaxCall(..)` 是「error-first」風格的回調(爲了簡潔,省略了部分代碼)
// 假設:`runGenerator(..)` 也具有錯誤處理的功能(爲了簡潔,省略了部分代碼)

function request(url) {
    return new Promise( function(resolve,reject){
        // 傳入一個error-first風格的回調函數
        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 );
} );

  在request(..)函數中,makeAjaxCall(..)若是出錯,會返回一個promise的rejection,並最終映射到generator函數的error(在runGenerator(..)函數中經過it.throw(..)方法拋出錯誤,這部分細節對於消費端來講是透明的),而後在消費端咱們經過try..catch語句最終捕獲錯誤。

  下面咱們來看一下複雜點的使用promises異步調用的狀況:

function request(url) {
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } )
    // 在ajax調用完以後獲取返回值,而後進行下一步操做
    .then( function(text){
        // 查看返回值中是否包含URL
        if (/^https?:\/\/.+/.test( text )) {
            // 若是有則繼續調用這個新的URL
            return request( text );
        }
        // 不然直接返回調用的結果
        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對象,它接收三個子promises,當全部的子promises都完成以後,將返回的結果經過yield表達式傳遞給runGenerator(..)函數並恢復運行。在request(..)函數中,每一個子promise經過鏈式操做對response的值進行解析,若是其中包含另外一個URL則繼續請求這個URL,若是沒有則直接返回response的值。有關promise的鏈式操做能夠查看這篇文章: http://blog.getify.com/promises-part-5/#the-chains-that-bind-us

  任何複雜的異步處理,你均可以經過在generator函數中使用yield promise來完成(或者promise的promise鏈式操做),這樣代碼具備同步風格,看起來更加簡潔。這是目前最佳的處理方式。

 

runGenerator(..)工具庫

  咱們須要定義咱們本身的runGenerator(..)工具來實現上面介紹的generator+promises模式。爲了簡單,咱們甚至能夠不用實現全部的功能,由於這其中有不少的細節須要處理,例如錯誤處理的部分。

  可是你確定不想親自來寫runGenerator(..)函數吧?反正我是不想。

  其實有不少的開源庫提供了promise/async工具,你能夠無償使用。這裏我就不去一一介紹了,推薦看看Q.spawn(..)co(..)等。

  這裏我想介紹一下我本身寫的一個工具庫:asynquence的插件runner。由於我認爲和其它工具庫比起來,這個插件提供了一些獨特的功能。我寫過一個系列文章,是有關asynquence的,若是你有興趣的話能夠去讀一讀。

  首先,asynquence提供了一系列的工具來自動處理「error-first」風格的回調函數。看下面的代碼:

function request(url) {
    return ASQ( function(done){
        // 這裏傳入了一個error-first風格的回調函數 - done.errfcb
        makeAjaxCall( url, done.errfcb );
    } );
}

  看起來是否是會好不少?

  接下來,asynquence的runner(..)插件消費了asynquence序列(異步調用序列)中的generator函數,所以你能夠從序列的從上一步中傳入消息,而後generator函數能夠將這個消息返回,繼續傳到下一步,而且這其中的任何錯誤都將自動向上拋出,你不用本身去管理。來看看具體的代碼:

// 首先調用`getSomeValues()`建立一個sequence/promise,
// 而後將sequence中的async鏈起來
getSomeValues()

// 使用generator函數來處理獲取到的values
.runner( function*(token){
    // token.messages數組將會在前一步中賦值
    var value1 = token.messages[0];
    var value2 = token.messages[1];
    var value3 = token.messages[2];

    // 並行調用3個Ajax請求,並等待它們所有執行完(以任何順序)
    // 注意:`ASQ().all(..)`相似於`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 )
    );

    // 將message發送到下一步
    yield (msgs[0] + msgs[1] + msgs[2]);
} )

// 如今,將前一個generator函數的最終結果發送給下一個請求
.seq( function(msg){
    return request( "http://some.url.4?msg=" + msg );
} )

// 全部的所有執行完畢!
.val( function(result){
    console.log( result ); // 成功,所有完成!
} )

// 或者,有錯誤發生!
.or( function(err) {
    console.log( "Error: " + err );
} );

  Asynquence runner(..)從sequence的上一步中接收一個messages(可選)來啓動generator,這樣在generator中能夠訪問token.messages數組中的元素。而後,與咱們上面演示的runGenerator(..)函數同樣,runner(..)負責監聽yield promise或者yield asynquence(一個ASQ().all(..)包含了全部並行的步驟),等待完成以後再恢復generator函數的運行。當generator函數運行完以後,最終的結果將會傳遞給sequence中的下一步。此外,若是這其中有錯誤發生,包括在generator函數體內產生的錯誤,都將會向上拋出或者被錯誤處理程序捕捉到。

  Asynquence試圖將promises和generator融合到一塊兒,使代碼編寫變得很是簡單。只要你願意,你能夠隨意地將任何generator函數與基於promise的sequence聯繫到一塊兒。

ES7 async

  在ES7的計劃中,有一個提案很是不錯,它建立了另一種function:async function。有點像generator函數,它會自動包裝到一個相似於咱們的runGenerator(..)函數(或者asynquence的runner(..)函數)的utility中。這樣,就能夠自動地發送promisesasync function並在它們執行完後恢復運行(甚至都不須要generator函數遍歷器了!)。

  代碼看起來就像這樣:

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 function能夠被直接調用(上面代碼中的main()語句),而不用像咱們以前那樣須要將它包裝到runGenerator(..)或者ASQ.runner(..)函數中。在函數內部,咱們不須要yield,取而代之的是await(另外一個新加入的關鍵字),它會告訴async function等待promise完成以後纔會繼續運行。未來咱們會有更多的generator函數庫都支持本地語法。

  是否是很酷?

  同時,像asynquence runner這樣的庫同樣,它們會給咱們在異步generator函數編程方面帶來極大的便利。

 

總結

  一句話,generator + yield promise(s)模式功能是如此強大,它們一塊兒使得對同步和異步的流程控制變得行運自如。伴隨着使用一些包裝庫(不少現有的庫都已經免費提供了),咱們能夠自動執行咱們的generator函數直到全部的任務所有完成,而且包含了錯誤處理!

  在ES7中,咱們極可能將會看到async function這種類型的函數,它使得咱們在沒有第三方庫支持的狀況下也能夠作到上面說的這些(至少對於一些簡單狀況來講是能夠的)。

  JavaScript的異步在將來是光明的,並且只會愈來愈好!我堅信這一點。

  不過還沒完,咱們還有最後一個東西須要探索:

  若是有兩個或多個generators函數,如何讓它們獨立地並行運行,而且各自發送本身的消息呢?這或許須要一些更強大的功能,沒錯!咱們管這種模式叫「CSP」(communicating sequential processes)。咱們將在下一篇文章中探討和揭祕CSP的強大功能。敬請關注!

相關文章
相關標籤/搜索