Promise對象
- 基本用法
Promise 新建後就會立即執行
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
上面代碼中,Promise 新建後立即執行,所以首先輸出的是Promise。然後,then方法指定的回調函數,將在當前腳本所有同步任務執行完纔會執行,所以resolved最後輸出。
調用resolve或reject並不會終結 Promise 的參數函數的執行。
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
上面代碼中,調用resolve(1)以後,後面的console.log(2)還是會執行,並且會首先打印出來。這是因爲立即 resolved 的 Promise 是在本輪事件循環的末尾執行,總是晚於本輪循環的同步任務。
一般來說,調用resolve或reject以後,Promise 的使命就完成了,後繼操作應該放到then方法裏面,而不應該直接寫在resolve或reject的後面。所以,最好在它們前面加上return語句,這樣就不會有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 後面的語句不會執行
console.log(2);
})
- Promise.prototype.catch()
一般來說,不要在then方法裏面定義 Reject 狀態的回調函數(即then的第二個參數),總是使用catch方法。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
上面代碼中,第二種寫法要好於第一種寫法,理由是第二種寫法可以捕獲前面then方法執行中的錯誤,也更接近同步的寫法(try/catch)。因此,建議總是使用catch方法,而不使用then方法的第二個參數。
跟傳統的try/catch代碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行會報錯,因爲x沒有聲明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
上面代碼中,someAsyncThing函數產生的 Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined,但是不會退出進程、終止腳本執行,2 秒之後還是會輸出123。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是“Promise 會喫掉錯誤”。
- Promise.prototype.finally()
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代碼中,不管promise最後的狀態,在執行完then或catch指定的回調函數以後,都會執行finally方法指定的回調函數。
- Promise.all()
Promise.all方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.all([p1, p2, p3]);
上面代碼中,Promise.all方法接受一個數組作爲參數,p1、p2、p3都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。(Promise.all方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。)
p的狀態由p1、p2、p3決定,分成兩種情況。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p1、p2、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。
如果作爲參數的 Promise 實例,自己定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了]
上面代碼中,p1會resolved,p2首先會rejected,但是p2有自己的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的實際上是這個實例。該實例執行完catch方法後,也會變成resolved,導致Promise.all()方法參數裏面的兩個實例都會resolved,因此會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。
如果p2沒有自己的catch方法,就會調用Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('報錯了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了
- Promise.race()
Promise.race方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。
const p = Promise.race([p1, p2, p3]);
上面代碼中,只要p1、p2、p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。
Promise.race方法的參數與Promise.all方法一樣,如果不是 Promise 實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。
- Promise.resolve()
有時需要將現有對象轉爲 Promise 對象,Promise.resolve方法就起到這個作用
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代碼將 jQuery 生成的deferred對象,轉爲一個新的 Promise 對象。
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’)則是立即執行,因此最先輸出。
- Promise.reject()
Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected。
const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
上面代碼生成一個 Promise 對象的實例p,狀態爲rejected,回調函數會立即執行。
注意,Promise.reject()方法的參數,會原封不動地作爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
上面代碼中,Promise.reject方法的參數是一個thenable對象,執行以後,後面catch方法的參數不是reject拋出的“出錯了”這個字符串,而是thenable對象。
- Promise.try()
實際開發中,經常遇到一種情況:不知道或者不想區分,函數f是同步函數還是異步操作,但是想用 Promise 來處理它。因爲這樣就可以不管f是否包含異步操作,都用then方法指定下一步流程,用catch方法處理f拋出的錯誤。一般就會採用下面的寫法。
Promise.resolve().then(f)
上面的寫法有一個缺點,就是如果f是同步函數,那麼它會在本輪事件循環的末尾執行。
const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now
上面代碼中,函數f是同步的,但是用 Promise 包裝了以後,就變成異步執行了。
那麼有沒有一種方法,讓同步函數同步執行,異步函數異步執行,並且讓它們具有統一的 API 呢?回答是可以的,並且還有兩種寫法。第一種寫法是用async函數來寫。
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
上面代碼中,第二行是一個立即執行的匿名函數,會立即執行裏面的async函數,因此如果f是同步的,就會得到同步的結果;如果f是異步的,就可以用then指定下一步,就像下面的寫法。
(async () => f())()
.then(...)
需要注意的是,async () => f()會喫掉f()拋出的錯誤。所以,如果想捕獲錯誤,要使用promise.catch方法。
(async () => f())()
.then(...)
.catch(...)
第二種寫法是使用new Promise()。
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
上面代碼也是使用立即執行的匿名函數,執行new Promise()。這種情況下,同步函數也是同步執行的。
鑑於這是一個很常見的需求,所以現在有一個提案,提供Promise.try方法替代上面的寫法。
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
事實上,Promise.try就是模擬try代碼塊,就像promise.catch模擬的是catch代碼塊。
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)