首頁  >  文章  >  web前端  >  詳細介紹JavaScript非同步程式設計的Promise模式範例程式碼

詳細介紹JavaScript非同步程式設計的Promise模式範例程式碼

黄舟
黄舟原創
2017-03-11 15:19:571210瀏覽

非同步模式在web程式設計中變得越來越重要,對於web主流語言Javascript來說,這種模式實現起來不是很利索,為此,許多Javascript庫(例如jQuery和Dojo)添加了一種稱為promise的抽象(有時也稱為deferred)。透過這些函式庫,開發人員能夠在實際程式設計中使用 promise模式。 IE官方部落格最近發表了一篇文章,詳細講述如何使用XMLHttpRequest2來實踐promise模式。讓我們來了解一下相關的概念和應用。

考慮這樣一個例子,某網頁有非同步操作(透過XMLHttpRequest2或 Web Workers)。隨著Web 2.0技術的深入,瀏覽器端承受了越來越多的運算壓力,所以「並發」具有正面的意義。對於開發人員來說,既要保持頁面與使用者的互動不受影響,又要協調頁面與非同步任務的關係,這種非線性執行的程式要求存在適應的困難。先拋開頁面互動不談,我們能夠想到對於非同步呼叫需要處理兩種結果──成功操作和失敗處理。在成功的呼叫後,我們可能需要把回傳的結果用在另一個Ajax請求中,這就會出現「函數連環套」的情況(在筆者的另一篇文章《NodeJS的非同步程式設計風格》中有詳細的解釋)。這種情況會造成程式設計的複雜性。看看下面的程式碼範例(基於XMLHttpRequest2):

function searchTwitter(term, onload, onerror) {

     var xhr, results, url;
     url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
     xhr = new XMLHttpRequest();
     xhr.open('GET', url, true);

     xhr.onload = function (e) {
         if (this.status === 200) {
             results = JSON.parse(this.responseText);
             onload(results);
         }
     };

     xhr.onerror = function (e) {
         onerror(e);
     };

     xhr.send();
 }

 function handleError(error) {
     /* handle the error */
 }

 function concatResults() {
     /* order tweets by date */
 }

 function loadTweets() {
     var container = document.getElementById('container');

     searchTwitter('#IE10', function (data1) {
         searchTwitter('#IE9', function (data2) {
             /* Reshuffle due to date */
             var totalResults = concatResults(data1.results, data2.results);
             totalResults.forEach(function (tweet) {
                 var el = document.createElement('li');
                 el.innerText = tweet.text;
                 container.appendChild(el);
             });
         }, handleError);
     }, handleError);
 }

上面的程式碼其功能是取得Twitter中hashtag為IE10和IE9的內容並在頁面中顯示出來。這種嵌套的回呼函數難以理解,開發人員需要仔細分析哪些程式碼用於應用的業務邏輯,而哪些程式碼處理非同步函數呼叫的,程式碼結構支離破碎。錯誤處理也分解了,我們需要在各個地方檢測錯誤的發生並作出相應的處理。

為了降低非同步程式設計的複雜性,開發人員一直在尋找簡單的方法來處理非同步操作。其中一種處理模式稱為promise,它代表了一種可能會長時間運行且不一定必須完整的操作的結果。這種模式不會阻塞和等待長時間的操作完成,而是傳回一個代表了承諾的(promised)結果的物件。

考慮這樣一個例子,頁面程式碼需要存取第三方的API,網路延遲可能會造成回應時間較長,在這種情況下,採用非同步程式設計不會影響整個頁面與使用者的互動。 promise模式通常會實作一種稱為then的方法,用來註冊狀態變化時對應的回呼函數。例如下面的程式碼範例:

searchTwitter(term).then(filterResults).then(displayResults);

promise模式在任何時刻都處於以下三種狀態之一:未完成(unfulfilled)、已完成(resolved)和拒絕(rejected)。以CommonJS Promise/A 標準為例,promise物件上的then方法負責新增針對已完成和拒絕狀態下的處理函數。 then方法會傳回另一個promise對象,以便於形成promise管道,這種傳回promise物件的方式能夠支援開發人員把非同步操作串聯起來,如then(resolvedHandler, rejectedHandler); 。 resolvedHandler 回呼函數在promise物件進入完成狀態時會觸發,並傳遞結果;rejectedHandler函數會在拒絕狀態下呼叫。

有了promise模式,我們可以重新實作上面的Twitter範例。為了更好的理解實作方法,我們嘗試著從零開始建立一個promise模式的框架。首先需要一些物件來儲存promise。

var Promise = function () {
        /* initialize promise */
    };

接下來,定義then方法,接受兩個參數用於處理完成和拒絕狀態。

Promise.prototype.then = function (onResolved, onRejected) {
     /* invoke handlers based upon state transition */
 };

同時也需要兩個方法來執行理從未完成到已完成和從未完成到拒絕的狀態轉變。

Promise.prototype.resolve = function (value) {
     /* move from unfulfilled to resolved */
 };

 Promise.prototype.reject = function (error) {
     /* move from unfulfilled to rejected */
 };

現在建造了一個promise的架子,我們可以繼續上面的範例,假設只取得IE10的內容。建立一個方法來傳送Ajax請求並將其封裝在promise中。這個promise物件分別在xhr.onload和xhr.onerror中指定了完成和拒絕狀態的轉變過程,請注意searchTwitter函數傳回的正是promise物件。然後,在loadTweets中,使用then方法設定完成和拒絕狀態對應的回呼函數。

function searchTwitter(term) {

    var url, xhr, results, promise;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    promise = new Promise();
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);

    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            promise.resolve(results);
        }
    };

    xhr.onerror = function (e) {
        promise.reject(e);
    };

    xhr.send();
    return promise;
}

function loadTweets() {
    var container = document.getElementById('container');
    searchTwitter('#IE10').then(function (data) {
        data.results.forEach(function (tweet) {
            var el = document.createElement('li');
            el.innerText = tweet.text;
            container.appendChild(el);
        });
    }, handleError);
}

到目前為止,我們可以把promise模式應用於單一Ajax請求,似乎還體現不出promise的優勢來。下面來看看多個Ajax請求的並發協作。此時,我們需要另一個方法when來儲存準備呼叫的promise物件。一旦某個promise從未完成狀態轉換為完成或拒絕狀態,then方法裡對應的處理函數就會被呼叫。 when方法在需要等待所有操作都完成的時候至關重要。

Promise.when = function () {
    /* handle promises arguments and queue each */
};

以剛才取得IE10和IE9兩塊內容的場景為例,我們可以這樣來寫程式碼:

var container, promise1, promise2;
container = document.getElementById('container');
promise1 = searchTwitter('#IE10');
promise2 = searchTwitter('#IE9');
Promise.when(promise1, promise2).then(function (data1, data2) {

    /* Reshuffle due to date */
    var totalResults = concatResults(data1.results, data2.results);
    totalResults.forEach(function (tweet) {
        var el = document.createElement('li');
        el.innerText = tweet.text;
        container.appendChild(el);
    });
}, handleError);

分析上面的代码可知,when函数会等待两个promise对象的状态发生变化再做具体的处理。在实际的Promise库中,when函数有很多变种,比如 when.some()、when.all()、when.any()等,读者从函数名字中大概能猜出几分意思来,详细的说明可以参考CommonJS的一个promise实现when.js。

除了CommonJS,其他主流的Javascript框架如jQuery、Dojo等都存在自己的promise实现。开发人员应该好好利用这种模式来降低异步编程的复杂性。我们选取Dojo为例,看一看它的实现有什么异同。

Dojo框架里实现promise模式的对象是Deferred,该对象也有then函数用于处理完成和拒绝状态并支持串联,同时还有resolve和reject,功能如之前所述。下面的代码完成了Twitter的场景:

function searchTwitter(term) {

    var url, xhr, results, def;
    url = 'http://search.twitter.com/search.json?rpp=100&q=' + term;
    def = new dojo.Deferred();
    xhr = new XMLHttpRequest();
    xhr.open('GET', url, true);

    xhr.onload = function (e) {
        if (this.status === 200) {
            results = JSON.parse(this.responseText);
            def.resolve(results);
        }
    };

    xhr.onerror = function (e) {
        def.reject(e);
    };

    xhr.send();
    return def;
}

dojo.ready(function () {
    var container = dojo.byId('container');
    searchTwitter('#IE10').then(function (data) {
        data.results.forEach(function (tweet) {
            dojo.create('li', {
                innerHTML: tweet.text
            }, container);
        });
    });
});

不仅如此,类似dojo.xhrGet方法返回的即是dojo.Deferred对象,所以无须自己包装promise模式。

var deferred = dojo.xhrGet({
    url: "search.json",
    handleAs: "json"
});

deferred.then(function (data) {
    /* handle results */
}, function (error) {
    /* handle error */
});

除此之外,Dojo还引入了dojo.DeferredList,支持开发人员同时处理多个dojo.Deferred对象,这其实就是上面所提到的when方法的另一种表现形式。

dojo.require("dojo.DeferredList");
dojo.ready(function () {
    var container, def1, def2, defs;
    container = dojo.byId('container');
    def1 = searchTwitter('#IE10');
    def2 = searchTwitter('#IE9');

    defs = new dojo.DeferredList([def1, def2]);

    defs.then(function (data) {
        // Handle exceptions
        if (!results[0][0] || !results[1][0]) {
            dojo.create("li", {
                innerHTML: 'an error occurred'
            }, container);
            return;
        }
        var totalResults = concatResults(data[0][1].results, data[1][1].results);

        totalResults.forEach(function (tweet) {
            dojo.create("li", {
                innerHTML: tweet.text
            }, container);
        });
    });
});

上面的代码比较清楚,不再详述。

说到这里,读者可能已经对promise模式有了一个比较完整的了解,异步编程会变得越来越重要,在这种情况下,我们需要找到办法来降低复杂度,promise模式就是一个很好的例子,它的风格比较人性化,而且主流的JS框架提供了自己的实现。所以在编程实践中,开发人员应该尝试这种便捷的编程技巧。需要注意的是,promise模式的使用需要恰当地设置promise对象,在对应的事件中调用状态转换函数,并且在最后返回promise对象。

技术社区对异步编程的关注也在升温,国内社区也发出了自己的声音。资深技术专家老赵就发布了一套开源的异步开发辅助库Jscex,它的设计很巧妙,抛弃了回调函数的编程方式,采用一种“线性编码、异步执行”的思想,感兴趣的读者可以查看这里。

不仅仅是前端的JS库,如今火热的NodeJS平台也出现了许多第三方的promise模块,具体的清单可以访问这里。

以上是詳細介紹JavaScript非同步程式設計的Promise模式範例程式碼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn