JavaScript的Callback機制深入人心。而ECMAScript的世界同樣充斥的各種異步操作(異步IO、setTimeout等)。異步和Callback的搭載很容易就衍生"回調金字塔"。——由此產生Deferred/Promise。
Deferred起源於Python,后來被CommonJS挖掘並發揚光大,得到了大名鼎鼎的Promise,並且已經納入ECMAScript 6(JavaScript下一版本)。
Promise/Deferred是當今最著名的異步模型,不僅強壯了JavaScript Event Loop(事件輪詢)機制下異步代碼的模型,同時增強了異步代碼的可靠性。—— 匠者為之,以惠匠者。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
這段代碼的運行邏輯是這樣的:
我們的總是遇見這樣的情況:一段代碼異步執行,后續的代碼卻需要等待異步代碼的,如果在異步代碼之前執行,就會如上面的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);
});
上面的代碼,從代碼語義上達到了下面的流程:
我們僅觀察代碼就知道現在的它變得非常優雅,兩次異步的代碼被完美的抹平。但我們應該時刻謹記,Promise改變的是你異步的代碼和編程思想,而並沒有改變異步代碼的執行——它是一種由卓越的編程思想所衍生的對象。
下面一張圖演示了普通異步回調和Promise異步的區別,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的前生今世。