Promises與Javascript異步程式設計
轉載:http://www.zawaliang.com/2013/08/399.html
在如今都追求使用者體驗的時代,Ajax應用真的是無所不在。加上這些年浏覽器技術、HTML5以及CSS3等的發展,越來越多的富Web應用出現;在給與我們良好體驗的同時,Web開發人員在背後需要處理越來越多的異步回調邏輯。
筆者對最近讀完的《Async Javascript-Build More Responsive Apps with Less Code》(Javascript異步程式設計-設計快速響應的網絡應用)一書以及部分資料,整理了我認為比較重要的一些點以及容易了解錯的地方,使大家對 Promise對象以及異步程式設計有更深的認識。
嵌套式回調
setTimeout(function() {
setTimeout(function() {
// do something
}, 10)
}, 100);
$.ajax(url, function() {
$.ajax(url2, function() {
$.ajax(url3, function() {
// do something
});
});
});
可是問題來了,當我們的嵌套越多,代碼結構層級會變得越來越深。首先是閱讀上會變得困難;其次是強耦合,接口變得不好擴充。我們需要一種模式來解決這種問題,這就是Promises所要做的事情。
異步函數類型
Javascript裡異步函數可以分為兩大類型:
- I/O函數(Ajax、script…)
- 計時函數(setTimeout、setInterval、setImmediate)
異步函數異常捕獲
try {
setTimeout(function A() {
setTimeout(function B() {
setTimeout(function C() {
throw new Error('Error');
}, 0);
}, 0);
}, 0);
} catch (e) {}
運作以上代碼,A、B、C被添加到事件隊列裡;異常觸發時,A、B已被移出事件隊列,記憶體堆棧裡隻存在C,此時的異常不被try捕獲,隻會流向應用程式未捕獲異常處理器。
是以,在異步函數裡,不能使用try/catch捕獲異常。
分布式事件
Javascript的事件核心是事件分發機制,通過對釋出者綁定訂閱句柄來達到異步相響應的目的:
document.onclick = function() {
// click
};
PubSub(Publish/Subscribe, 釋出/訂閱)模式,就是這麼一種模式,通過訂閱釋出者的事件響應來達到多層分發解耦的目的。
以下是一個簡單版本的PubSub模式實作:
var PubSub = (function() {
var _handlers = {};
return {
// 訂閱事件
on: function(eventType, handler) {
if (!_handlers[eventType]) {
_handlers[eventType] = [];
}
if (typeof handler == 'function') {
_handlers[eventType].push(handler);
}
},
//釋出事件
emit: function(eventType) {
var args = Array.prototype.slice.call(arguments, 1);
var handlers = _handlers[eventType] || [];
for (var i = 0, len = handlers.length; i < len; i++) {
handlers[i].apply(null, args)
}
}
};
})();
Promises/A規範
CommonJS之Promises/A規範是Kris Zyp于2009年提出來的,它通過規範API接口來簡化異步程式設計,使我們的異步邏輯代碼更易了解。
遵循Promises/A規範的實作我們稱之為Promise對象,Promise對象有且僅有三種狀态:unfulfilled(未完成)、 fulfilled(已完成)、failed(失敗/拒絕);而且狀态變化隻能從unfulfilled到fulfilled,或者 unfulfilled到failed;
Promise對象需實作一個then接口,then(fulfilledHandler, errorHandler, progressHandler);then接口接收一個成功回調(fulfilledHandler)與一個失敗回調(errorHandler);progressHandler觸發回調是可選的,Promise對象沒有強制去回調此句柄。
then方法的實作需要傳回一個新的Promise對象,以形成鍊式調用,或者叫Promise管道。
為了實作狀态的轉變,我們還需要實作另外兩個接口:
- resolve:實作狀态由未完成到已完成
- reject:實作狀态由未完成到拒絕(失敗)
這樣子我們開篇所說的嵌套式回調就可以這樣子寫了:
// 這裡假設Promise是一個已實作的Promise對象
function asyncFn1() {
var p = new Promise();
setTimeout(function() {
console.log(1);
p.resolve(); // 标記為已完成
}, 2000);
return p;
}
function asyncFn2() {
var p = new Promise();
setTimeout(function() {
console.log(2);
p.reject('error'); // 标記為拒絕
}, 1000);
return p;
}
asyncFn1()
.then(function() {
return asyncFn2();
}).then(function() {
console.log('done');
}, function(err) {
console.log(err);
});
有了Promise,我們可以以同步的思維去編寫異步的邏輯了。在同步函數的世界裡,有2個非常重要的概念:
有傳回值
可以抛出異常
Promise不僅僅是一種可以鍊式調用的對象,更深層次裡,它為異步函數與同步函數提供了一種更加直接的對應關系。
上面我們說過,在異步函數裡,不能使用try/catch捕獲異常,是以也不能抛出異常。有了Promise,隻要我們顯式定義了errorHandler,那麼我們就可以做到像同步函數那樣的異常捕獲了。