天天看點

JavaScript異步程式設計(1)- ECMAScript 6的Promise對象

JavaScript的Callback機制深入人心。而JavaScript世界同樣充斥的各種異步操作(異步IO、setTimeout等)。異步和Callback的搭載很容易就衍生"回調金字塔"。——由此産生Deferred/Promise。而Deferred起源于Python,後來被CommonJS挖掘并發揚光大,得到了大名鼎鼎的Promise,并且已經納入ECMAScript 6(JavaScript下一版本)。Promise/Deferred是當今最著名的異步模型,不僅強壯了JavaScript Event Loop(事件輪詢)機制下異步代碼的模型,同時增強了異步代碼的可靠性。匠者為之,以惠匠者。

JavaScript的Callback機制深入人心。而ECMAScript的世界同樣充斥的各種異步操作(異步IO、setTimeout等)。異步和Callback的搭載很容易就衍生"回調金字塔"。——由此産生Deferred/Promise。

Deferred起源于Python,後來被CommonJS挖掘并發揚光大,得到了大名鼎鼎的Promise,并且已經納入ECMAScript 6(JavaScript下一版本)。

Promise/Deferred是當今最著名的異步模型,不僅強壯了JavaScript Event Loop(事件輪詢)機制下異步代碼的模型,同時增強了異步代碼的可靠性。—— 匠者為之,以惠匠者。

> 本文内容如下:

>

> - Promise應對的問題

> - Promise的解決

> - ECMAScript 6 Promise

> - 參考和引用

Promise應對的問題

JavaScript充斥着Callback,例如下面的代碼:

(function (num) {//從外面接收一個參數
    var writeName = function (callback) {
        if (num === 1)
            callback();
    }
    writeName(function () {//callback
        console.log("i'm linkFly");
    });
})(1);
           

把一個函數通過參數傳遞,那麼這個函數叫做Callback(回調函數)。

JavaScript也充斥着異步操作——例如ajax。下面的代碼就是一段異步操作:

var name;
    setTimeout(function () {
        name = 'linkFly';
    }, 1000);//1s後執行
    console.log(name);//輸出undefined
           

這段代碼的運作邏輯是這樣的:

JavaScript異步程式設計(1)- ECMAScript 6的Promise對象

我們的總是遇見這樣的情況:一段代碼異步執行,後續的代碼卻需要等待異步代碼的,如果在異步代碼之前執行,就會如上面的console.log(name)一樣,輸出undefined,這并不是我們想要的效果。

類似的情況總是發生在我們經常要使用的ajax上:

$.ajax({
        url: 'http://www.cnblogs.com/silin6/map',
        success: function (key) {
            //我們必須要等待這個ajax加載完成才能發起第二個ajax
            $.ajax({
                url: 'http://www.cnblogs.com/silin6/source/' + key,
                success: function (data) {
                    console.log("i'm linkFly");//後輸出
                }
            });
        }
    });
    console.log('ok');//ok會在ajax之前執行
           

異步操作有點類似這一段代碼被挂起,先執行後續的代碼,直到異步得到響應(例如setTimeout要求的1s之後執行,ajax的伺服器響應),這一段異步的代碼才會執行。關于這一段異步代碼的執行流程,請參閱JavaScript大名鼎鼎的:Event Loop(事件輪詢)。

Promise的解決

Promise優雅的修正了異步代碼,我們使用Promise重寫我們setTimeout的示例:

var name,
    p = new Promise(function (resolve) {
        setTimeout(function () {//異步回調
            resolve();
        }, 1000);//1s後執行
    });
    p.then(function () {
        name = 'linkFly';
        console.log(name);//linkFly
    }).then(function () {
        name = 'cnBlog';
        console.log(name);
    });
    //這段代碼1s後會輸出linkFly,cbBlog
           

我們先不要太過在意Promise對象的API,後續會講解,我們隻需要知道這段代碼完成了和之前同樣的工作。我們的console.log(name)正确的輸出了linkFly,并且我們還神奇的輸出了cnBlog。

或許你覺得這段代碼實在繁瑣,還不如setTimeout來的痛快,那麼我們再來改寫上面的ajax:

var ajax = function (url) {
        //我們改寫ajax,讓它以Promise的方式工作
        return new Promise(function (resolve) {
            $.ajax({
                url: url,
                success: function (data) {
                    resolve(data);
                }
            });
        });
    };
    ajax('http://www.cnblogs.com/silin6/map')
        .then(function (key) {
            //我們得到key,發起第二條請求
            return ajax('http://www.cnblogs.com/silin6/source/' + key);
        })
        .then(function (data) {
            console.log(data);//這時候我們會接收到第二次ajax傳回的資料
        });
           

或許它晦澀難懂,那麼我們嘗試用setTimeout來模拟這次的ajax,這個例子示範了Promise資料的傳遞,一如ajax:

var name,
        ajax = function (data) {
            return new Promise(function (resolve) {
                setTimeout(function () {//我們使用setTimeout模拟ajax
                    resolve(data);
                }, 1000);//1s後執行
            });
        };

    ajax('linkFly').then(function (name) {
        return ajax("i'm " + name);//模拟第二次ajax
    }).then(function (value) {
        //2s後,輸出i'm linkFly
        console.log(value);
    });
           

上面的代碼,從代碼語義上達到了下面的流程:

JavaScript異步程式設計(1)- ECMAScript 6的Promise對象

我們僅觀察代碼就知道現在的它變得非常優雅,兩次異步的代碼被完美的抹平。但我們應該時刻謹記,Promise改變的是你異步的代碼和程式設計思想,而并沒有改變異步代碼的執行——它是一種由卓越的程式設計思想所衍生的對象。

下面一張圖示範了普通異步回調和Promise異步的差別,Promise實作的異步從代碼運作上來說并無太大差別,但從程式設計思想上來說差異巨大。

JavaScript異步程式設計(1)- ECMAScript 6的Promise對象

ECMAScript 6 Promise##

Promise對象代表了未來某個将要發生的事件(通常是一個異步操作),抹平了異步代碼的金字塔,它從模型上解決了異步代碼産生的"回調金字塔"。

Promise是ECMAScript 6規範内定義的,是以請使用現代浏覽器測試,它的相容性可以在這裡檢視。

Promise.constructor

Promise是一個對象,它的構造函數接收一個回調函數,這個回調函數參數有兩個函數:分别在成功狀态下執行和失敗狀态下執行,Promise有三個狀态,分别為:等待态(Pending)、執行态(Fulfilled)和拒絕态(Rejected)。

var p = new Promise(function (resolve,reject) {
        console.log(arguments);
        //resolve表示成功狀态下執行
        //reject表示失敗狀态下執行
    });
           

傳遞的這個回調函數,等同被Promise重新封裝,并傳遞了兩個參數回調,這兩個參數用于驅動Promise資料的傳遞。resolve和reject本身承載着觸發器的使命:

  • 預設的Promise對象是等待态(Pending)。
  • 調用resolve()表示這個Promise進入執行态(Fulfilled)
  • 調用reject()表示這個promise()進入拒絕态(Rejected)
  • Promise對象可以從等待狀态下進入到執行态和拒絕态,并且無法回退。
  • 而執行态和拒絕态不允許互相轉換(例如執行态轉換到拒絕态)。

Promise.prototype.then

生成的promise執行個體(如上面的變量p)擁有方法then(),then()方法是Promise對象的核心,它傳回一個新的Promise對象,是以可以像jQuery一樣鍊式操作,非常優雅。

Promise是雙鍊的,是以then()方法接受兩個參數,分别表示:

  • _執行态(Fulfilled)_下執行的回調函數
  • _拒絕态(Rejected)_下執行的回調函數。
p.then(function () {
      //我們傳回一個promise
      return new Promise(function (resolve) {
            setTimeout(function () {
                resolve('resolve');
            }, 1000);//異步1s
        });
        
    }, function () {
        console.log('rejected');
    })  //鍊式回調
        .then(function (state) {
            console.log(state);//如果為執行态,輸出resolve
        }, function (data) {
            console.log(data);//如果為拒絕态,輸出undefined
        });;
           

then()方法的傳回值由它相應狀态下執行的函數決定:這個函數傳回undefined,則then()方法建構一個預設的Promise對象,并且這個對象擁有then()方法所屬的Promise對象的狀态。

var p = new Promise(function (resolve) {
        resolve();//直接标志執行态
    }), temp;
    temp = p.then(function () {
        //傳入執行态函數,不傳回值
    });
    temp.then(function () {
        console.log('fulfilled');//擁有p的狀态
    });

    console.log(temp === p);//預設建構的promise,但已經和p不是同一個對象,輸出false
           

如果對應狀态所執行的函數傳回一個全新的Promise對象,則會覆寫掉目前Promise,代碼如下:

var p = new Promise(function (resolve) {
        resolve();//直接标志執行态
    }), temp;
    temp = p.then(function () {
        //傳回新的promise對象,和p的狀态無關
        return new Promise(function (resolve, reject) {
            reject();//标志拒絕态
        });
    });
    temp.then(function () {
        console.log('fulfilled');
    }, function () {
        console.log('rejected');//輸出
    });
           

即then()方法傳遞的進入的回調函數,如果傳回promise對象,則then()方法傳回這個promise對象,否則将預設建構一個新的promise對象,并繼承調用then()方法的promise的狀态。

我們應該清楚Promise的使命,抹平了異步代碼的回調金字塔,我們會有很多依賴上一層異步的代碼:

var url = 'http://www.cnblogs.com/silin6/';
    ajax(url, function (data) {
        ajax(url + data, function (data2) {
            ajax(url + data2, function (data3) {
                ajax(url + data3, function () {
                    //回調金字塔
                });
            });
        });
    });
           

使用Promise則抹平了代碼:

promise.then(function (data) {
        return ajax(url + data);
    }).then(function (data2) {
        return ajax(url + data2);
    }).then(function (data3) {
        return ajax(url + data3);
    }).then(function (data) {
        //扁平化代碼
    });
           

Promise還有更多更強大的API。但本文的目的旨在讓大家感受到Promise的魅力,而并非講解Promise對象自身的API,關于Promise其他輔助實作API請查閱本文最下方的引用章節,Promise其他API如下:

  • Promise.prototype.catch():用于指定發生錯誤時的回調函數(捕獲異常),并具有冒泡性質。
  • Promise.all(),Promise.race():Promise.all方法用于将多個Promise執行個體,包裝成一個新的Promise執行個體。
  • Promise.resolve(),Promise.reject():将現有對象轉為Promise對象。

希望大家一點點的接受Promise,是以沒有講太多,我們對于Promise的了解不應該僅僅是一個異步模型,我們更關注應該是Promise/Deferred的程式設計思想,是以後續幾篇會逐漸深入講解Promise的前生今世。

參考和引用##

  • ECMAScript 6 入門 - Promise對象
  • Promise/A+規範
  • JavaScript架構設計-第12章 異步處理
  • Promise啟示錄

作者:linkFly

原文:http://www.cnblogs.com/silin6/p/4288967.html

出處:www.cnblogs.com/silin6/

聲明:嘿!你都拷走上面那麼一大段了,我覺得你應該也不介意順便拷走這一小段,希望你能夠在每一次的引用中都保留這一段聲明,尊重作者的辛勤勞動成果,本文與部落格園共享。

繼續閱讀