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,例如下面的程式碼:
1 2 3 4 5 6 7 8 9 |
(function (num) {//從外面接收一個引數 var writeName = function (callback) { if (num === 1) callback(); } writeName(function () {//callback console.log("i'm linkFly"); }); })(1); |
把一個函式通過引數傳遞,那麼這個函式叫做Callback(回撥函式)。
JavaScript也充斥著非同步操作——例如ajax。下面的程式碼就是一段非同步操作:
1 2 3 4 5 |
var name; setTimeout(function () { name = 'linkFly'; }, 1000);//1s後執行 console.log(name);//輸出undefined |
這段程式碼的執行邏輯是這樣的:
我們的總是遇見這樣的情況:一段程式碼非同步執行,後續的程式碼卻需要等待非同步程式碼的,如果在非同步程式碼之前執行,就會如上面的console.log(name)一樣,輸出undefined,這並不是我們想要的效果。
類似的情況總是發生在我們經常要使用的ajax上:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$.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的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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)。
1 2 3 4 5 |
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)下執行的回撥函式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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物件的狀態。
1 2 3 4 5 6 7 8 9 10 11 |
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,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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的使命,抹平了非同步程式碼的回撥金字塔,我們會有很多依賴上一層非同步的程式碼:
1 2 3 4 5 6 7 8 9 10 |
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則抹平了程式碼:
1 2 3 4 5 6 7 8 9 |
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的前生今世。