一篇須要膜拜的文篇--Javascript異步編程模型進化(轉)

要我能用得這麼熟,javascript

那前端出師了哈。前端

http://foio.github.io/javascript-asyn-pattern/java

改天一個一個親測一下。node

Javascript語言是單線程的,沒有複雜的同步互斥;可是,這並無限制它的使用範圍;相反,藉助於Node,Javascript已經在某些場景下具有通吃先後端的能力了。近幾年,多線程同步IO的模式已經在和單線程異步IO的模式的對決中敗下陣來,Node也所以得名。接下來咱們深刻介紹一下Javascript的殺手鐗,異步編程的發展歷程。git

讓咱們假設一個應用場景:一篇文章有10個章節,章節的數據是經過XHR異步請求的,章節必須按順序顯示。咱們從這個問題出發,逐步探求從粗糙到優雅的解決方案。github


1.回憶往昔之callback

在那個年代,javascript僅限於前端的簡單事件處理,這是異步編程的最基本模式了。 好比監聽dom事件,在dom事件發生時觸發相應的回調。ajax

element.addEventListener('click',function(){ //response to user click }); 

好比經過定時器執行異步任務。編程

setTimeout(function(){ //do something 1s later }, 1000); 

可是這種模式註定沒法處理複雜的業務邏輯的。假設有N個異步任務,每個任務必須在上一個任務完成後觸發,因而就有了以下的代碼,這就產生了回調黑洞。json

doAsyncJob1(function(){ doAsyncJob2(function(){ doAsyncJob3(function(){ doAsyncJob4(function(){ //Black hole }); }) }); }); 

2.活在當下之promise

針對上文的回調黑洞問題,有人提出了開源的promise/A+規範,具體規範見以下地址:https://promisesaplus.com/。promise表明了一個異步操做的結果,其狀態必須符合下面幾個要求:後端

一個Promise必須處在其中之一的狀態:pending, fulfilled 或 rejected.
若是是pending狀態,則promise能夠轉換到fulfilled或rejected狀態。
若是是fulfilled狀態,則promise不能轉換成任何其它狀態。
若是是rejected狀態,則promise不能轉換成任何其它狀態。

2.1 promise基本用法

promise有then方法,能夠添加在異步操做到達fulfilled狀態和rejected狀態的處理函數。

promise.then(successHandler,failedHandler); 

而then方法同時也會返回一個promise對象,這樣咱們就能夠鏈式處理了。

promise.then(successHandler,failedHandler).then().then(); 

MDN上的一張圖,比較清晰的描述了Pomise各個狀態之間的轉換。

假設上文中的doAsyncJob都返回一個promise對象,那咱們看看如何用promise處理回調黑洞:

doAsyncJob1().then(function(){ return doAsyncJob2();; }).then(function(){ return doAsyncJob3(); }).then(function(){ return doAsyncJob4(); }).then(//......); 

這種編程方式是否是清爽多了。咱們最常用的jQuery已經實現了promise規範,在調用$.ajax時能夠寫成這樣了:

var options = {type:'GET',url:'the-url-to-get-data'}; $.ajax(options).then(function(data){ //success handler },function(data){ //failed handler }); 

咱們可使用ES6的Promise的構造函數生成本身的promise對象,Promise構造函數的參數爲一個函數,該函數接收兩個函數(resolve,reject)做爲參數,並在成功時調用resolve,失敗時調用reject。以下代碼生成一個擁有隨機結果的promise。

var RandomPromiseJob = function(){ return new Promise(function(resolve,reject){ var res = Math.round(Math.random()*10)%2; setTimeout(function(){ if(res){ resolve(res); }else{ reject(res); } }, 1000) }); } RandomPromiseJob().then(function(data){ console.log('success'); },function(data){ console.log('failed'); }); 

jsfiddle演示地址:http://jsfiddle.net/panrq4t7/

promise錯誤處理也十分靈活,在promise構造函數中發生異常時,會自動設置promise的狀態爲rejected,從而觸發相應的函數。

new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).then(undefined,function(data){ console.log(data.message); }); 

其中then(undefined,function(data)能夠簡寫爲catch。

new Promise(function(resolve,reject){ resolve(JSON.parse('I am not json')); }).catch(function(data){ console.log(data.message); }); 

jsfiddle演示地址:http://jsfiddle.net/x696ysv2/

2.2 一個更復雜的例子

promise的功能毫不僅限於上文這種小打小鬧的應用。對於篇頭提到的一篇文章10個章節異步請求,順序展現的問題,若是使用回調處理章節之間的依賴邏輯,顯然會產生回調黑洞; 而使用promise模式,則代碼形式優雅並且邏輯清晰。假設咱們有一個包含10個章節內容的數組,並有一個返回promise對象的getChaper函數:

var chapterStrs = [ 'chapter1','chapter2','chapter3','chapter4','chapter5', 'chapter6','chapter7','chapter8','chapter9','chapter10', ]; var getChapter = function(chapterStr) { return get('<p>' + chapterStr + '</p>', Math.round(Math.random()*2)); }; 

下面咱們探討一下如何優雅高效的使用promise處理這個問題。

(1). 順序promise

順序promise主要是經過對promise的then方法的鏈式調用產生的。

//按順序請求章節數據並展現 chapterStrs.reduce(function(sequence, chapterStr) { return sequence.then(function() { return getChapter(chapterStr); }).then(function(chapter) { addToPage(chapter); }); }, Promise.resolve()); 

這種方法有一個問題,XHR請求是串行的,沒有充分利用瀏覽器的並行性。網絡請求timeline和顯示效果圖以下:

 

查看jsfiddle演示代碼: http://jsfiddle.net/81k9nv6x/1/

(2). 併發promise,一次性

Promise類有一個all方法,其接受一個promise數組:

Promise.all([promise1,promise2,...,promise10]).then(function(){ }); 

只有promise數組中的promise所有兌現,纔會調用then方法。使用Promise.all,咱們能夠併發性的進行網絡請求,並在全部請求返回後在集中進行數據展現。

//併發請求章節數據,一次性按順序展現章節 Promise.all(chapterStrs.map(getChapter)).then(function(chapters){ chapters.forEach(function(chapter){ addToPage(chapter); }); }); 

這種方法也有一個問題,要等到全部數據加載完成後,纔會一次性展現所有章節。效果圖以下:

查看jsfiddle演示代碼:http://jsfiddle.net/7ops845a/

(3). 併發promise,漸進式

其實,咱們能夠作到併發的請求數據,儘快展現知足順序條件的章節:即前面的章節展現後就能夠展現當前章節,而不用等待後續章節的網絡請求。基本思路是:先建立一批並行的promise,而後經過鏈式調用then方法控制展現順序。

chapterStrs.map(getChapter).reduce(function(sequence, chapterStrPromise) { return sequence.then(function(){ return chapterStrPromise; }).then(function(chapter){ addToPage(chapter); }); }, Promise.resolve()); 

效果以下:

 

查看jsfiddle演示代碼:http://jsfiddle.net/fuog1ejg/

這三種模式基本上歸納了使用Pormise控制併發的方式,你能夠根據業務需求,肯定各個任務之間的依賴關係,從而作出選擇。

2.3 promise的實現

ES6中已經實現了promise規範,在新版的瀏覽器和node中咱們能夠放心使用了。對於ES5及其如下版本,咱們能夠藉助第三方庫實現,q(https://github.com/kriskowal/q)是一個很是優秀的實現,angular使用的就是它,你能夠放心使用。下一篇文章準備實現一個本身的promise。


3.憧憬將來之generater

異步編程的一種解決方案叫作"協程"(coroutine),意思是多個線程互相協做,完成異步任務。隨着ES6中對協程的支持,這種方案也逐漸進入人們的視野。Generator函數是協程在 ES6 的實現.

3.1 Generator三大基本特性

讓咱們先從三個方面瞭解generator。

(1) 控制權移交

在普通函數名前面加*號就能夠生成generator函數,該函數返回一個指針,每一次調用next函數,就會移動該指針到下一個yield處,直到函數結尾。經過next函數就能夠控制generator函數的執行。以下所示:

function *gen(){ yield 'I'; yield 'love'; yield 'Javascript'; } var g = gen(); console.log(g.next().value); //I console.log(g.next().value); //love console.log(g.next().value); //Javascript 

next函數返回一個對象{value:'love',done:false},其中value表示yield返回值,done表示generator函數是否執行完成。這樣寫有點low?試試這種語法。

for(var v of gen()){ console.log(v); } 

(2) 分步數據傳遞

next()函數中能夠傳遞參數,做爲yield的返回值,傳遞到函數體內部。這裏有點tricky,next參數做爲上一次執行yeild的返回值。理解「上一次」很重要。

function* gen(x){ var y = yield x + 1; yield y + 2; return 1; } var g = gen(1); console.log(g.next()) // { value: 2, done: false } console.log(g.next(2)) // { value: 4, done: true } console.log(g.next()); //{ value: 1, done: true } 

好比這裏的g.next(2),參數2爲上一步yield x + 1 的返回值賦給y,從而咱們就能夠在接下來的代碼中使用。這就是generator數據傳遞的基本方法了。

(3) 異常傳遞

經過generator函數返回的指針,咱們能夠向函數內部傳遞異常,這也使得異步任務的異常處理機制獲得保證。

function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); console.log(g.next()); //{ value: 3, done: false } g.throw('error'); //error 

3.2 用generator實現異步操做

仍然使用本文中的getChapter方法,該方法返回一個promise,咱們看一下如何使用generator處理異步回調。gen方法在執行到yield指令時返回的result.value是promise對象,而後咱們經過next方法將promise的結果返回到gen函數中,做爲addToPage的參數。

function *gen(){ var result = yield getChapter('I love Javascript'); addToPage(result); } var g = gen(); var result = g.next(); result.value.then(function(data){ g.next(data); }); 

gen函數的代碼,和普通同步函數幾乎沒有區別,只是多了一條yield指令。

jsfiddle地址以下:http://jsfiddle.net/fhnc07rq/3/

3.3 使用co進行規範化異步操做

雖然gen函數自己很是乾淨,只須要一條yield指令便可實現異步操做。可是我卻須要一堆代碼,用於控制gen函數、向gen函數傳遞參數。有沒有更規範的方式呢?其實只須要將這些操做進行封裝,co庫爲咱們作了這些(https://github.com/tj/co)。那麼咱們用generator和co實現上文的逐步加載10個章節數據的操做。

function *gen(){ for(var i=0;i<chapterStrs.length;i++){ addToPage(yield getChapter(chapterStrs[i])); } } co(gen); 

jsfiddle演示地址:http://jsfiddle.net/0hvtL6e9/

這種方法的效果相似於上文中提到「順序promise」,咱們能不能實現上文的「併發promise,漸進式」呢?代碼以下:

function *gen(){ var charperPromises = chapterStrs.map(getChapter); for(var i=0;i<charperPromises.length;i++){ addToPage(yield charperPromises[i]); } } co(gen); 

jsfiddle演示地址: http://jsfiddle.net/gr6n3azz/1/

經歷過複雜性才能達到簡單性。咱們從最開始的回調黑洞到最終的generator,愈來愈複雜也愈來愈簡單。

===================

    function *gen() {
        yield 'I';
        yield 'love';
        yield 'Javascript';
    }
    
    var g = gen();
    console.log(g.next().value);
    console.log(g.next().value);
    console.log(g.next().value);
    
    function *gen1(x) {
        var y = yield x + 1;
        yield y + 2;
        return 1;
    }
    
    var g1 = gen1(3);
    console.log(g1.next());
    console.log(g1.next(10));
    console.log(g1.next());

相關文章
相關標籤/搜索