系列文章 -- ES6筆記系列 很久很久以前,在做Node.js聊天室,使用MongoDB數據服務的時候就遇到了多重回調嵌套導致代碼混亂的問題。 JS非同步編程有利有弊,Promise的出現,改善了這一格局,讓非同步編程表現出類似“同步式代碼”的形式,更好地體現了它的價值。 一、基本概念 1. Prom ...
系列文章 -- ES6筆記系列
很久很久以前,在做Node.js聊天室,使用MongoDB數據服務的時候就遇到了多重回調嵌套導致代碼混亂的問題。
JS非同步編程有利有弊,Promise的出現,改善了這一格局,讓非同步編程表現出類似“同步式代碼”的形式,更好地體現了它的價值。
一、基本概念
1. Promises/A+規範
Promise是一種非同步編程的解決方案,本質來說其實它是一種規範,Promises/A+規範
根據規範的定義,一個Promise對象應該至少有以下的基本特點
三個狀態
Promise有三個狀態:Pending(進行中)、Resolved或Fulfilled(已完成)、Rejected(已失敗)
其中:Pending為Promise的初始狀態;當Resolved成功時,會調用onFulfilled方法;當Rejected失敗時,會調用onRejected方法
並且:狀態只能從Pending轉換為Resolved狀態,或者從Pending轉換為Rejected狀態,不存在其他狀態間的轉換
Then方法
Promise必須提供一個then方法,用以訪問當前值、最終的返回值以及失敗的原因
最基本的then方法接受兩個函數參數 promise.then(onFulfilled, onReject),對應於狀態變化到Resolved和Rejected時的函數調用
2. Promise簡單的實現
基於Promises/A+中規範的要求,可以自行實現一個基本的promise對象
可參考 一步一步實現一個Promise
二、基本使用
1. 使用相關插件
近年來,已經出現了很多Promise非同步編程的插件,我們可以使用這些插件,常見的有:
例如使用jQuery新版Ajax模塊內置的Deferred使用到了Promise,我們可以直接這樣調用
// 回調函數的方式 $.get('url', function(rs) { rs = JSON.parse(rs); }); // Promise的形式 $.get('url').success(function(rs) { rs = JSON.parse(rs); })
不過jQuery中的Promise並不是完全按照Primises/A+規範來實現的,所以使用的時候可能會有問題,詳見
2. 原生的Promise支持
ES6原生引入了Promise,它在很多現代瀏覽器上已經得到支持
在不支持原生Promise的環境下,除了可以直接使用一些第三方Promise庫之外,還可以使用這個插件來相容低版本瀏覽器
其實,ES6中的原生Promise實現與RSVP.js有很大的關係,所以相關語法也和它類似
比如在爬蟲開發時,先獲取用戶資料,再獲取該用戶的文章,則可以用Promise,用類似以下的結構實現
function getUser(id) { return new Promise(function(resolve, reject) { $.get('/user?id=' + id, function(rs) { rs = JSON.parse(rs); if (rs.status !== 200) { reject(rs); } else { resolve(rs); } }); }); } function getContent(user) { return new Promise(function(resolve, reject) { $.get('/content', { user: user }, function(rs) { rs = JSON.parse(rs); if (rs.status !== 200) { reject(rs); } else { resolve(rs); } }); }); } getUser(11).then(function(rs) { return getContent(rs.user); }).catch(function(rs) { throw Error(rs.msg); }).then(function(rs) { console.log(rs.content); }).catch(function(rs) { throw Error(rs.msg); });
成功調用getUser之後,可以通過return 返回getContent(rs.user)這個promise對象,繼續接下去的執行任務
除了直接返回這個新的promise對象,我們也可以直接返回一個數據,這個數據將會作為下一函數調用時的參數,且看例子:
function step(num) { return new Promise(function(resolve, reject) { setTimeout(function() { if (num > 0) { resolve(num); } else { reject(0); } }) }); } step(-1).then(function(num) { console.log('resolve ' + num); return -5; }, function(num) { console.log('reject ' + num); // reject 0 return 5; }).then(step) // 下一個要執行的任務操作 .then(function(num) { console.log('resolve ' + num); // resolve 5 }, function(num) { console.log('reject ' + num); });
當參數的數值為正數時,則直接resolve返回該數值,如果為負數則reject返回0,初始數值為-1,所以調用了reject
再看另一個例子:
function log(n) { return new Promise(function(resolve, reject) { setTimeout(function() { if (n % 2) { resolve('奇數:' + n); } else { reject('偶數:' + n); } }, 1000); }); } log(1).then(function(data) { console.log(data); return log(2); }, function(err) { console.log(err); return log(3); }).then(function(data) { console.log(data); }, function(err) { console.log(err); });
以上代碼執行之後
下麵來詳細介紹原生Promise的使用方法
new Promise(func)
通過實例化構造函數成一個promise對象,構造函數中有個函數參數,函數參數為(resolve, reject)的形式,供以函數內resolve成功以及reject失敗的調用
.then(onFulfilled, onRejected)
then方法,方法帶兩個參數,可選,分別為成功時的回調以及失敗時的回調
如上代碼,log(1)時執行了resolve,log(2)時執行了reject
.catch(onRejected)
catch方法,方法帶一個參數,為失敗時的回調。其實.catch方法就是 .then(undefined, onRejected)的簡化版,通過例子看看它的特點
function log(n) { return new Promise(function(resolve, reject) { setTimeout(function() { if (n % 2) { resolve('奇數:' + n); } else { reject('偶數:' + n); } }, 1000); }); } log(2).then(function(data) { console.log(data); return log(3); }).catch(function(err) { console.log(err); });
看這個例子,then中只有一個參數,調用log(2)之後reject執行,到達catch中輸出
再看一個慄子,代碼換成以下兩種,輸出都一樣
log(1).then(function(data) { console.log(data); return log(2); }).catch(function(err) { console.log(err); });
log(1).then(function(data) { console.log(data); return log(2); }).then(undefined, function(err) { console.log(err); });
如此一來,第一輪log(1)的resolve後,自行調用log(2),從而執行reject,通過catch執行相應的輸出
Promise.all()方法
Promise.all()方法接受一個promise的數組對象,只有數組中所有的promise都執行成功,整個promise才算成功,如果數組對象中有個promise執行失敗,則整個promise就失敗
看這個簡單的例子,意圖是調用log(1,2,3,4,5)這個promise完成之後再調用log(6),其中相應值小於3就resolve,反之就reject
1 function log() { 2 var promises = []; 3 4 [...arguments].forEach(function(n) { 5 promises.push(new Promise(function(resolve, reject) { 6 7 var info = ''; 8 setTimeout(function() { 9 if (n < 3) { 10 info = '小於3 resolve:' + n; 11 console.log(info); 12 resolve(info); 13 } else { 14 info = 'reject:' + n; 15 console.log(info); 16 reject(info); 17 } 18 }, 1000); 19 })); 20 }); 21 22 return Promise.all(promises); 23 } 24 25 log(1, 2, 3, 4, 5).then(function(data) { 26 console.log(data); 27 return log(6); 28 }).catch(function(err) { 29 console.log(err); 30 });
首先,依次將相應實例化的promise對象存入promises數組,通過Promise.all()調用返回,執行結果為
由輸出結果知,1和2被resolve,3、4、5被reject,整個數組裡已經有多於一個的promise對象被reject,僅僅觸發了catch中的回調,所以log(6)得不到執行
Promise.race()方法
與Promise.all()類似,它也接受一個數組對象作為參數,但其意義不一樣
只有數組中所有的promise都執行失敗,整個promise才算失敗,如果數組對象中有個promise執行成功,則整個promise就成功
把上述代碼的all換成race,執行結果為:
由輸出結果知,1和2被resolve,3、4、5被reject,整個數組裡已經有多於一個的promise對象被resolve,觸發了then中成功的回調,log(6)得到調用執行
因為這時還沒有額外的then或catch方法來監視log(6)的狀態,所以僅僅輸出的在log函數中執行的結果
Promise.resolve()方法
除了在實例化Promise構造函數內部使用resolve之外,我們還可以直接調用resolve方法
var promise = Promise.resolve('resolve one'); // var promise = Promise.reject('reject one'); promise.then(function(data) { console.log(data); // resolve one }).catch(function(err) { console.log(err); });
參數除了可以直接指定值之外,還可以是一個Promise實例,具有then方法的對象,或者為空
參數為Promise實例,則將包裝後返回該Promise實例
var promise = Promise.resolve($.get('url'));
前文說到jQuery的Promise實現方式並不是完全按照規範來著,通過Promise.resolve的包裝,可以返回一個規範化的Promise實例
參數為空,則直接返回一個狀態為resolved|fulfilled的Promise對象
setTimeout(function () { console.log('three'); }, 0); Promise.resolve().then(function () { console.log('two'); }); console.log('one');
直接resolve的Promise對象是在本輪事件迴圈結束時執行,setTimeout是在下一輪事件迴圈結束時執行,所以輸出為:
參數為一個具有then方法的對象,則自動將這個對象轉換為Promise對象並調用該then方法,如
Promise.resolve({ then: function(resolve, reject) { resolve('resolved'); } }).then(function(data) { console.log(data); // resolved }).catch(function(err) { console.log(err); });
Promise.reject()方法
除了在實例化Promise構造函數內部使用reject之外,我們還可以直接調用reject方法
類似於Promise.resolve()中參數的多樣化,且看以下幾個慄子:
Promise.resolve({ then: function(resolve, reject) { reject('rejected'); } }).then(function(data) { console.log(data); }).catch(function(err) { console.log(err); // rejected });
setTimeout(function () { console.log('three'); }, 0); Promise.reject().catch(function () { console.log('two'); }); console.log('one'); // one // two // three
var promise = Promise.reject($.get('url'));
// var promise = Promise.resolve('resolve one'); var promise = Promise.reject('reject one'); promise.then(function(data) { console.log(data); }).catch(function(err) { console.log(err); // reject one });
3. Promise的反模式
關於Promise有很多難點技巧點,比如以下四中調用方式的區別
doSomething().then(function () { return doSomethingElse(); }); doSomethin().then(functiuoin () { doSomethingElse(); }); doSomething().then(doSomethingElse()); doSomething().then(doSomethingElse);
相關解釋見:談談使用 promise 時候的一些反模式