非同步模式在web編程中變得越來越重要,對於web主流語言Javscript來說,這種模式實現起來不是很利索,為此,許多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模組,具體的清單可以訪問這裡。
註:本文中的所有程式碼範例均來自於IE官方部落格。
崔康 是InfoQ中文站的翻譯主編,從事企業級Web應用的相關工作,關注效能最佳化、Web技術、瀏覽器等領域。
轉自:http://www.infoq.com/cn/news/2011/09/js-promise