JavaScript精進之路 — 異步的實現(上)

要帶著問題學,活學活用,學用結合,急用先學,立竿見影,在「用」字上狠下功夫。java

廢話少說。
這是這個專題的第二部份內容,異步。主要總結了《你不知道的JavaScript(中卷)》中有關於異步的內容。顯然一會兒寫完三個部分的內容不太可能,下篇會在不久以後放出。
因爲前人之述備矣,因此有些地方會引用它山之石,它山之石能夠攻玉嘛。 ?git

什麼是異步

首先明確,JavaScript是一種單線程語言,不會出現多線程。github

1. 【異步的核心】

程序中如今運行部分和未來運行部分的關係就是異步編程的核心。簡單來說,若是程序中出現了一部分要在如今運行(順序同步執行),一部分要在未來運行(多是設置了timeout也多是一個ajax的異步調用後執行的函數),那麼二者之間的關係的構建就構成了異步編程。web

2. 【事件循環】

至關於一個永遠執行的while(true)循環,循環的每一輪稱爲一個tick。對於每一個tick而言,若是隊列中有等待事件,那麼從隊列中拿下這個事件執行。隊列中事件就是註冊的異步調用函數。
因爲事件循環的緣由,setTimeout只是在timeout的時間後將函數註冊到事件循環中,由於有被其餘任務阻塞的可能,因此其時間不必定準確。setInterval同理可得。
setTimeout(…,0)能夠進行異步調動,將函數放在事件隊列循環的末尾,是一種hack的方法。
具體能夠參閱如下blog:你所不知道的setInterval | 晚晴幽草軒ajax

3. 【任務】

Promise的then是基於任務的。任務和事件循環的區別,能夠理解爲任務表明的異步函數能夠插隊進入當前事件以後。因此從理論上來講,任務循環(job loop)可能致使無限循環(一個任務添加另外一個不須要排隊的任務,例如Promise中then的無限鏈接)使得沒法進入到下一個tick中。編程

EX 事件循環和任務的認識數組

(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()

輸出是 1 2 3 5 4 而非 1 2 3 4 5
這就說明了Promise決議以後,先執行了then的這個任務(job),這個then沒有進入事件循環中排隊,由於若是排隊,應該會在setTimeout這個先註冊的function以後調用。因此then的任務隊列的優先級高於事件循環。而且磁力還說明了Promise的決議過程是同步執行的。promise

具體的原理說明:
https://github.com/creeperyan...多線程

4. 【異步交互協調】

有時會因爲兩個ajax調用的前後順序(或者其餘操做的前後順序)的緣由會致使運行結果的不一樣,爲了控制進程的執行,有兩種控制的模式和兩種簡單的方式:
首先是:這個能夠控制兩個函數都完成以後才進行下一步工做,條件控制條件爲if(a && b)
第二種是競態,也可稱爲門閂。就是兩個函數只有一個可以被調用,另外一個會被忽略,其控制條件是設置一個undefined的變量a,調用後設爲有值,而且判斷if(!a)併發

異步的基礎模式 — 回調(callback)

回調能夠說是JavaScript的基礎了,這裏不講回調的好處,只有回調的幾個明顯缺點(不然則麼顯現出後面的進化呢(笑)):

1. 【回調函數】

回調函數封裝了程序的延續(continuation)。回調函數是處理JavaScript異步邏輯最基礎的方法,但也有着各類的缺點。

2. 【嵌套回調和鏈式回調(回調地獄)】

有下列代碼:

//《你不知道的JavaScript(中卷)》
listen( "click", function handler(evt){
    setTimeout( function request(){
        ajax( "http://some.url.1", function response(text){
            if (text == "hello") {
                handler();
            }
            else if (text == "world") {
                request();
            }
        } );
    }, 500) ;
} );

這是一個由三個函數嵌套在一塊兒的鏈式回調,每一個函數表明了一個異步序列。

因爲回調的特性,可能很難一下看出這個函數的執行邏輯(缺少順序性),因此又被稱爲回調地獄或者毀滅金字塔。

【回調地獄的缺陷】:

doA( function(){
    doC();

    doD( function(){
        doF();
    } )

    doE();
} );

doB();

若是函數A和D是異步執行的,那麼這個回調過程的執行步驟是A - F - B - C - E - D

除了難以閱讀之外,回調地獄真正的問題在於一旦指定了全部的可能時間和路徑,代碼就會變得十分複雜,沒法維護和更新。由於一個進行的回調要是可以覆蓋全部路徑,可能會寫上不少並行的回調函數,在代碼中看起來可能會十分凌亂和難以調試維護。

3. 【控制反轉】

這牽涉到異步程序設計的信任問題。

控制反轉就是程序執行的主動權從本身的手中交了出去。若是僅僅是簡單的ajax調用,那麼這個控制切換可能不會帶來什麼大問題。但若是將一個回調函數交給一個外部的API,由於沒法查看的具體代碼,因此能夠看作是一個黑箱。這個黑箱致使問題是沒法調試,不知道這個外部程序到底怎樣調用了這個回調函數,是一次都沒有,仍是調用了不少次,亦或是比預想中過早過晚的調用,最終可能的後果就是程序執行的結果不如所願。

教科書一點的定義就是把本身程序一部分的執行控制交給了某個第三方,且與這個第三方之間沒有一份明確表達的契約。

由於回調沒有機制來保障這個必然出現的控制反轉的問題,這就成爲了回調的最大問題,會致使信任鏈的徹底斷裂,是程序出錯。

回調函數必須遵照的原則就是:信任,但要覈實。(Trust But Verify.)

4. 【error-first風格】

回調函數的第一個參數留給錯誤處理,若是成功第一個參數就置爲false,不然爲true。回調執行時先進行判斷。
可是這個風格並無徹底解決信任的問題,若是同時成功和失敗,就要另外寫代碼來處理。

5. 【Zalgo】

回調會有同步回調調用和異步回調調用。這樣也會產生程序的運行問題,見下列代碼:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;

這端代碼會有0(同步回調調用)仍是1(異步回調調用)的結果就要看狀況而定了
對於可能同步調用也可能異步調用給出的回調函數的第三方工具而言,這個信任問題是明顯的。雖然能夠用臃腫的附加代碼來解決,但並不優雅。

這樣的同步異步的混淆產生了另外一條準則:
**永遠要異步調用回調,即便只在事件的下一輪。
(always invoke callbacks asynchronously, even if that's "right away" on the next turn of the event loop)**

異步的進化一 Promise

前面一部分已經描述到了回調函數的兩個問題分別是:缺少順序性和缺少可信任性。

那麼這部分的Promise主要用來解決了可信任性的問題。

1. 【解決可信任問題的範式】

不把程序的控制權交給第三方,而是但願第三方提供一個瞭解其任務什麼時候結束的能力,而後由咱們的代碼來決定接下來作什麼。

2. 【將來值】

A對於B有一個承諾,若是A給出了任務完成能夠兌現承諾或者失敗不能兌現承諾的值,那麼這個值就稱爲將來值,簡單而言就是要在將來才能肯定的值,但有承諾保證這個值存在。

因爲將來值可能有兩個可能,要麼成功,要麼失敗。因此Promise值的then方法(在Promise值肯定以後調用的函數)就能夠接收兩個參數,第一個爲成功的話執行的函數,第二個爲失敗的話執行的函數。

舉個例子:

把x和y相加,若是有一個值沒有準備好,那就等待。一旦所有準備好就相加返回。

爲了統一處理未來和如今,就把他們所有變成將來值,就所有異步調用。

回調模式下的代碼:

function add(getX,getY,cb) {
    var x, y;
    getX( function(xVal){
        x = xVal;
        // both are ready?
        if (y != undefined) {
            cb( x + y );    // send along sum
        }
    } );
    getY( function(yVal){
        y = yVal;
        // both are ready?
        if (x != undefined) {
            cb( x + y );    // send along sum
        }
    } );
}

// `fetchX()` and `fetchY()` are sync or async
// functions
add( fetchX, fetchY, function(sum){
    console.log( sum ); // that was easy, huh?
} );

Promise模式下的代碼:

function add(xPromise,yPromise) {
    // `Promise.all([ .. ])` takes an array of promises,
    // and returns a new promise that waits on them
    // all to finish
    return Promise.all( [xPromise, yPromise] )

    // when that promise is resolved, let's take the
    // received `X` and `Y` values and add them together.
    .then( function(values){
        // `values` is an array of the messages from the
        // previously resolved promises
        return values[0] + values[1];
    } );
}

// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
add( fetchX(), fetchY() )

// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(..)` to wait for the
// resolution of that returned promise.
.then( function(sum){
    console.log( sum ); // that was easier!
} );

經過比較明顯看出Promise模式的方法能夠簡潔的表達一些操做。

Promise封裝了依賴於時間的狀態(等待將來值的產生,不管是如今仍是將來產生,後續的步驟都是同樣的,解決了同步回調仍是異步回調的問題),其自己與時間無關,因此能夠按照可預測的方式組合。但Promise一旦決議,那麼永遠將會保持在這個狀態,成爲不變值,能夠隨時查看。

3. 【revealing-constructor】

一種產生Promise的模式,一般格式爲
new Promise (function (…){…}) ,傳入的函數將會被當即執行。

4. 【識別Promise】

識別Promise是否爲真正的Promise很重要。定義某種稱爲thenable的東西,將其定義爲任何具備then(..)方法的對象和函數,任何這樣的值就是Promise一致的thenable。若是Promise決議遇到了這樣的thenable的值,那麼就會被擱淺在這裏,致使難以追蹤的bug。

5. 【Promise解決信任問題的方法】

有五種回調致使的信任問題,分別來說:

  1. 調用過早: 因爲一個任務有時候同步完成,有時候異步完成。若是使用回調會致使Zalgo出現,使用Promise不管是當即決議的revealing-constructor模式,仍是異步執行的內容,都會基於最前面所講的任務隊列來進行異步調用,這樣就解決了調用過早的問題.

  2. 調用過晚:因爲同步then調用時不被容許的,因此,一個Promise被決議以後,這個Promise上全部的經過then(…)註冊的回調都會下一個異步時機點一次被當即調用。任意一個都沒法影響或延誤對其餘回調的調用(不能插隊)
    這裏第一個function第一次註冊了打印出A的then方法,打印出B的then方法,註冊完畢後進行任務隊列的處理,由於A先註冊,因此先執行。這裏又註冊了一個C的then方法,雖然p已經被決議,可是並不能當即調用(不能同步調用),仍是加入到任務隊列的最後,不中斷對B的執行。因此執行結果是A B C。第二個是即便是p當即決議了,可是then中的內容仍是被延遲到執行完全部同步內容以後運行。可是不一樣Promise值的回調順序是不可預測的,永遠不要依賴於不一樣Promise之間的回調順序來進行程序調度。

Ex:

p.then( function(){
    p.then( function(){
        console.log( "C" );
    } );
    console.log( "A" );
} );
p.then( function(){
    console.log( "B" );
} );
// A B C

function runme() {
  var i = 0;

  new Promise(function(resolve) {
    resolve();
  })
  .then(function() {
    i += 2;
  });
  alert(i);
} //0
  1. 回調未調用 : 沒有任何東西(包括JavaScript錯誤)能夠組織Promise決議,它總會調用resolve和reject處理方法中的一個,即便是超時也有超時模式進行處理。(後續會講到)

  2. 調用次數過多或過少:因爲Promise只能被決議一次,註冊的then只會被最多調用一次,因此過多的調用會直接無效。過少就是以前解釋的回調未調用的狀況。

  3. 未能傳遞參數值、環境值:任何Promise都只能有一個決議值,若是resolve(…)或者reject(…)中傳遞了過多的參數,那都只會採納第一個,而忽略其餘的,若是要有多個,那麼就要封裝到數組或者對象中傳遞。

  4. 吞掉錯誤或異常:若是一個Promise產生了拒絕值而且給出了理由,那麼這個就會被傳給拒絕回調,即便是JavaScript的異常也會這樣作。這裏的會產生的另外一個細節就是若是發生JavaScript錯誤會致使的同步調用,因爲Promise的特性也會將其變爲異步的調用。
    可是試想,若是在then的正確處理函數中出現了錯誤會發生什麼?

EX:

var p = new Promise( function(resolve,reject){
    resolve( 42 );
} );

p.then(
    function fulfilled(msg){
        foo.bar();
        console.log( msg );    // never gets here :(
    },
    function rejected(err){
        // never gets here either :(
    }
);

因爲第一個then中未定義bar函數,因此會產生一個錯誤,可是並不會當即處理,而是會產生另外一個Promise,這個新的Promise會因爲錯誤而被拒絕,並無吞掉錯誤。由於p已經被決議爲正確,因此不會由於fulfilled中間有錯誤而去調用rejected。

  1. Promise.resolve()方法產生的Promise保證了返回內容的可信任性
    分別考慮resolve方法的參數,1)若是是一個非Promise,非thenable的 當即值,那麼就會返回一個用這個值填充的Promise封裝,保證了內容的可信任。(即便是錯誤值) 2)若是是一個Promise,那麼也只會產生一個Promise。3)若是傳遞了一個thenable的非Promise,那麼就會試圖展開這個值,直到遇到了一個符合1條件的當即值,並封裝爲Promise

經過這個方法,能夠保證異步返回給回調函數的值爲Promise可信任的。

6. 【鏈式流】

鏈式流能夠應用在會進行屢次異步調用的方法中,能夠增強代碼的清晰度可讀性和快速定位錯誤。
參見下面兩個代碼段:

//來自:http://imweb.io/topic/57a0760393d9938132cc8da9
getUserAdmin().then(function(result) {
    if ( /*管理員*/ ) {
        getProjectsWithAdmin().then(function(result) {
            /*根據項目id,獲取模塊列表*/
            getModules(result.ids).then(function(result) {
                /*根據模塊id,獲取接口列表*/
                getInterfaces(result.ids).then(function(result) {
                    // ...
                })
            })
        })
    } else {
        //...
    }
})


//鏈式流
getUserAdmin().then(function(reult) {
    if ( /*管理員*/ ) {
        return getProjectsWithAdmin();
    } else {
        return getProjectsWithUser();
    }
}).then(function(result) {
    /*獲取project id列表*/
    return getModules(result.ids);
}).then(function(result) {
    /*獲取project id列表*/
    return getInterfaces(result.ids)
}).then(function(result) {
    // ...
})

可以產生鏈式流基於如下兩個Promise的特性:
1.每次對Promise調用then(…),它都會產生一個新的Promise。

2.無論從then(…)調用的完成回調(第一個參數)返回的值是什麼,它都會被自動設置爲被鏈接Promise的完成,這句話表述了這個新的Promise的值就是這個then調用方法裏的return語句,若是沒有,那麼這個Promise的值就是undefined。
考慮如下代碼:

var p = Promise.resolve( 21 );

p
.then( function(v){
    console.log( v );    // 21

    // fulfill the chained promise with value `42`
    return v * 2;
} )
// here's the chained promise
.then( function(v){
    console.log( v );    // 42
} );

上面的代碼充分展示了這兩條規則。另外兩條則充分說明了即便是返回一個Promise甚至返回中有異步調用(這裏的異步調用不會被放入事件循環的最後,而是在這裏直接延遲執行,後續的then會等待其執行完畢),這兩條規則都會正常工做:

var p = Promise.resolve( 21 );

p.then( function(v){
    console.log( v );    // 21

    // create a promise and return it
    return new Promise( function(resolve,reject){
        // fulfill with value `42`
        resolve( v * 2 );
    } );
} )
.then( function(v){
    console.log( v );    // 42
} );
var p = Promise.resolve( 21 );

p.then( function(v){
    console.log( v );    // 21

    // create a promise to return
    return new Promise( function(resolve,reject){
        // introduce asynchrony!
        setTimeout( function(){
            // fulfill with value `42`
            resolve( v * 2 );
        }, 100 );
    } );
} )
.then( function(v){
    // runs after the 100ms delay in the previous step
    console.log( v );    // 42
} );

若是鏈中有步驟出錯,會直接將這個錯誤封裝爲Promise傳入到鏈中的下一個錯誤處理方法中(緣由以前已經講過)。若是這個錯誤處理return了一個值,那麼這個值會被帶入到下一個then處理的正確處理方法中,若是return了一個Promise那麼就有可能會使得下一個then延遲調用。若是沒有return,那就默認return undefined,一樣也是正確處理中。

默認的拒絕處理函數:若是產生了錯誤,但沒有拒絕處理函數,那麼就會有默認的,默認的所作的事情就是拋出錯誤,那麼這個錯誤就會繼續向下直到有顯式的拒絕處理函數。
默認的接收處理函數:純粹將一個promise繼續向下傳遞。若是隻有拒絕處理能夠將簡寫爲:catch(function(err){…})

7. 【Promise的錯誤處理】

因爲Promise一旦被決議就再也不更改的特性,如下代碼可能會致使沒有錯誤處理函數來處理:

var p = Promise.resolve( 42 );

p.then(
    function fulfilled(msg){
        // numbers don't have string functions,
        // so will throw an error
        console.log( msg.toLowerCase() );
    },
    function rejected(err){
        // never gets here
    }
);

幾種解決方案(除了1都未被ES6標準實現):
1) 在最後加catch,這樣會致使的問題就是catch中的函數若是也有錯誤就沒法捕捉。
2)有個done函數,就算done函數有錯誤,也傳入done中。

8. 【Promise模式】

以前介紹了兩種併發的模式,這裏有Promise來直接實現:
1) 門:幾個均實現再繼續進行: Promise.all([….]),參數能夠是由當即值,thenable或者Promise組成的數組。

注意:若是傳入空數組,那麼接下來的內容就會被當即設定爲完成。若是有Promise.all中有任意一個被拒絕,那麼整個都被拒絕,進入到拒絕處理函數。這個模式傳入到完成處理函數中的參數是一個數組,數組中的順序與all中聲明的順序相同,與其產生的順序無關。

2) 競態:幾個中只有一個能執行:Promise.race([…]),參數與all相同,可是若是是當即值的競爭那就會顯得毫無心義,第一個當即值會勝出。

注意:一旦有一個Promise被完成,那就所有完成,若是第一個是拒絕,那麼整個都被拒絕。若是傳遞空數組,那麼Promise會永遠都不會被決議。

3)超時模式的實現:以前講到了會有超時模式,這裏利用競態能夠來實現:

// `foo()` is a Promise-aware function

// `timeoutPromise(..)`, defined ealier, returns
// a Promise that rejects after a specified delay

// setup a timeout for `foo()`
Promise.race( [
    foo(),                    // attempt `foo()`
    timeoutPromise( 3000 )    // give it 3 seconds
] )
.then(
    function(){
        // `foo(..)` fulfilled in time!
    },
    function(err){
        // either `foo()` rejected, or it just
        // didn't finish in time, so inspect
        // `err` to know which
    }
);

4)幾種變體:

none:全部的Promise都是拒絕才是完成
any:只要有一個完成就是完成
first:只要第一個Promise完成,那麼整個就是完成
last:只有最後一個完成勝出

9. 【Promise的問題】

講了那麼多好處。。Promise固然也有問題:
1) 順序錯誤處理:可能會有錯誤被忽略而被全局拋出
2)單一值:只能有一個完成值、拒絕值,不然只能封裝解封,這樣會顯得有些笨重。(這個問題能夠經過ES6中的...運算來方便處理~)
3) 單決議:若是講一個決議綁定到會重複進行的操做上,那麼這個決議只會記住重複操做的第一次結果,如:

// `click(..)` binds the `"click"` event to a DOM element
// `request(..)` is the previously defined Promise-aware Ajax

var p = new Promise( function(resolve,reject){
    click( "#mybtn", resolve );
} );

p.then( function(evt){
    var btnID = evt.currentTarget.id;
    return request( "http://some.url.1/?id=" + btnID );
} )
.then( function(text){
    console.log( text );
} );
//第二次按下就不會有任何操做,不會再次執行resolve

4) 慣性:已經有不少回調的代碼不會天然的進行Promise改寫
5)沒法取消:若是Promise由於某些緣由懸而未決的話,沒法從外部阻止其繼續執行。
6)Promise會對性能有稍稍影響,但整體功大於過。

本文中的代碼除非有特別標註,均參考自:

https://github.com/getify/You...

相關文章
相關標籤/搜索