最近在整理js中異步編程方法時,回顧了一下promise,又發現了一些遺漏的重要知識點,好比promise.resolve()傳遞不一樣參數的含義?好比當一個promise依賴另外一個promise時事件執行順序?好比當catch捕獲到了錯誤後,會不會繼續執行後面的then方法?下文將對這些問題一一解答,並再次強調一些重要的知識點。es6
Promise編程的核心思想是若是數據就緒(promised),那麼(then)作點什麼。編程
下文是一個promise實例。json
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
複製代碼
Promise構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolve和reject。數組
resolve函數的做用是,將Promise對象的狀態從「未完成」變爲「成功」(即從 pending 變爲resolved),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去;promise
reject函數的做用是,將Promise對象的狀態從「未完成」變爲「失敗」(即從 pending 變爲rejected), 在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去。緩存
Promise實例生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。bash
promise.then(function(value) {
// success
}, function(error) {
// failure
});
複製代碼
then方法能夠接受兩個回調函數做爲參數。hexo
第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。其中,第二個函數是可選的,不必定要提供。這兩個函數都接受Promise對象傳出的值做爲參數。異步
下面是一個使用then的例子。then方法返回的是一個新的Promise實例。 所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。ide
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
複製代碼
上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數。
Promise.prototype.catch方法用於指定發生錯誤時的回調函數。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回調函數運行時發生的錯誤
console.log('發生錯誤!', error);
});
複製代碼
上面代碼中,getJSON方法返回一個 Promise 對象;若是異步操做拋出錯誤,狀態就會變爲rejected,就會調用catch方法指定的回調函數,處理這個錯誤。另外,then方法指定的回調函數,若是運行中拋出錯誤,也會被catch方法捕獲。
通常老是建議,Promise 對象後面要跟catch方法,這樣能夠處理 Promise 內部發生的錯誤。catch方法返回的仍是一個 Promise 對象,所以後面還能夠接着調用then方法。
注意:
1.若是調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。resolve函數的參數除了正常的值之外,還多是另外一個 Promise 實例。
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
複製代碼
上面代碼中,p1和p2都是 Promise 的實例,可是p2的resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。
這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。若是p1的狀態是pending,那麼p2的回調函數就會等待p1的狀態改變;若是p1的狀態已是resolved或者rejected,那麼p2的回調函數將會馬上執行。
2.調用resolve或reject並不會終結 Promise 的參數函數的執行。
3.then方法是定義在原型對象Promise.prototype上的。
4.若是沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。
5.Promise 在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。由於 Promise的狀態一旦改變,就永久保持該狀態,不會再變了。
有時須要將現有對象轉爲 Promise 對象,Promise.resolve方法就起到這個做用。
Promise.resolve等價於下面的寫法。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製代碼
Promise.resolve方法的參數分紅四種狀況。
1)參數是一個 Promise 實例
若是參數是 Promise 實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。
2)參數是一個thenable對象
thenable對象指的是具備then方法的對象,好比下面這個對象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
複製代碼
Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
複製代碼
上面代碼中,thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而當即執行最後那個then方法指定的回調函數,輸出 42。
3)參數不是具備then方法的對象,或根本就不是對象。
若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
複製代碼
上面代碼生成一個新的 Promise 對象的實例p。因爲字符串Hello不屬於異步操做(判斷方法是字符串對象不具備 then 方法),返回 Promise 實例的狀態從一輩子成就是resolved,因此回調函數會當即執行。Promise.resolve方法的參數,會同時傳給回調函數。
4)不帶有任何參數
Promise.resolve方法容許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。
因此,若是但願獲得一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法。
const p = Promise.resolve();
p.then(function () {
// ...
});
複製代碼
上面代碼的變量p就是一個 Promise 對象。須要注意的是,當即resolve的 Promise 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
複製代碼
上面代碼中,setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise. resolve()在本輪「事件循環」結束時執行,console.log('one')則是當即執行,所以最早輸出。
1)用了 promises 後怎麼用 forEach?
// 我想刪除全部的docs
db.allDocs({include_docs: true}).then(function (result) {
result.rows.forEach(function (row) {
db.remove(row.doc);
});
}).then(function () {
// 我天真的覺得全部的docs都被刪除了!
});
複製代碼
問題在於第一個函數實際上返回的是 undefined,這意味着第二個方法不會等待全部 documents 都執行 db.remove()。實際上他不會等待任何事情,而且可能會在任意數量的文檔被刪除後執行!
簡而言之,forEach()/for/while 並不是你尋找的解決方案。你須要的是 Promise.all():
db.allDocs({include_docs: true}).then(function (result) {
return Promise.all(result.rows.map(function (row) {
return db.remove(row.doc);
}));
}).then(function (arrayOfResults) {
// All docs have really been removed() now!
});
複製代碼
上面的代碼是什麼意思呢?大致來講,Promise.all()會以一個 promises 數組爲輸入,而且返回一個新的 promise。這個新的 promise 會在數組中全部的 promises 都成功返回後才返回。他是異步版的 for 循環。
而且 Promise.all() 會將執行結果組成的數組返回到下一個函數,好比當你但願從 PouchDB 中獲取多個對象時,會很是有用。此外一個更加有用的特效是,一旦數組中的 promise 任意一個返回錯誤,Promise.all() 也會返回錯誤。
2)忘記使用catch
單純的堅信本身的 promises 會永遠不出現異常,不少開發者會忘記在他們的代碼中添加一個 .catch()。然而不幸的是這也意味着,任何被拋出的異常都會被吃掉,而且你沒法在 console 中觀察到他們。這類問題 debug 起來會很是痛苦。
3) 使用反作用調用而非返回
下面的代碼有什麼問題?
somePromise().then(function () {
someOtherPromise();
}).then(function () {
// 我但願someOtherPromise() 狀態變成resolved!
// 可是並無
});
複製代碼
每個 promise 都會提供給你一個 then() 函數 (或是 catch(),實際上只是 then(null, ...) 的語法糖)。當咱們在 then() 函數內部時:
somePromise().then(function () {
// I'm inside a then() function! }); 複製代碼
咱們能夠作什麼呢?有三種事情:
就是這樣。一旦你理解了這個技巧,你就理解了 promises。
所以讓咱們逐個瞭解下。
返回另外一個 promise
getUserByName('nolan').then(function (user) {
return getUserAccountById(user.id);
}).then(function (userAccount) {
// I got a user account!
});
複製代碼
注意:我是 return
第二個 promise,這個 return
很是重要。若是我沒有寫 return
,getUserAccountById()
就會成爲一個反作用,而且下一個函數將會接收到 undefined
而非 userAccount
。
返回一個同步值 (或者 undefined)
返回 undefined 一般是錯誤的,可是返回一個同步值其實是將同步代碼包裹爲 promise 風格代碼的一種很是讚的手段。舉例來講,咱們對 users 信息有一個內存緩存。咱們能夠這樣作:
getUserByName('nolan').then(function (user) {
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
});
複製代碼
第二個函數不須要關心 userAccount 是從同步方法仍是異步方法中獲取的,而且第一個函數能夠很是自由的返回一個同步或者異步值。
拋出同步異常
好比咱們但願在用戶已經登出時,拋出一個同步異常。這會很是簡單:
getUserByName('nolan').then(function (user) {
if (user.isLoggedOut()) {
throw new Error('user logged out!'); // throwing a synchronous error!
}
if (inMemoryCache[user.id]) {
return inMemoryCache[user.id]; // returning a synchronous value!
}
return getUserAccountById(user.id); // returning a promise!
}).then(function (userAccount) {
// I got a user account!
}).catch(function (err) {
// Boo, I got an error!
});
複製代碼
若是用戶已經登出,咱們的 catch() 會接收到一個同步異常,而且若是 後續的 promise 中出現異步異常,他也會接收到。再強調一次,這個函數並不須要關心這個異常是同步仍是異步返回的。
http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/
http://es6.ruanyifeng.com/#docs/promise
原文見個人博客