ES6學習(四)
寫在前面
好久沒看es6了,昨天遇到一個與promise相關的bug,記錄下promise的相關知識了
正文
Promise物件是一個建構函式,用來生成Promise例項。
基礎用法
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
####非同步載入圖片
function loadImageAsync(url) { return new Promise(function(resolve, reject) { const image = new Image(); image.onload = function() { resolve(image); }; image.onerror = function() { reject(new Error('Could not load image at ' + url)); }; image.src = url; }); }
####Promise物件實現的 Ajax 操作 getJSON是對 XMLHttpRequest 物件的封裝,用於發出一個針對 JSON 資料的 HTTP 請求,並且返回一個Promise物件。需要注意的是,在getJSON內部,resolve函式和reject函式呼叫時,都帶有引數。
const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); }); return promise; }; getJSON("/posts.json").then(function(json) { console.log('Contents: ' + json); }, function(error) { console.error('出錯了', error); });
####注意小點
一般來說,呼叫resolve或reject以後,Promise 的使命就完成了,後繼操作應該放到then方法裡面,而不應該直接寫在resolve或reject的後面。所以,最好在它們前面加上return語句,這樣就不會有意外。
new Promise((resolve, reject) => {
return resolve(1);
// 後面的語句不會執行
console.log(2);
})
方法
Promise.prototype.then()
Promise 例項具有then方法,也就是說,then方法是定義在原型物件Promise.prototype上的。它的作用是為
Promise 例項新增狀態改變時的回撥函式。
then方法的第一個引數 resolved狀態的回撥函式, 第二個引數(可選) rejected狀態的回撥函式。
可以採用鏈式寫法 鏈式中後一個then的執行是要在前一個then返回的Promise物件變化時再執行。。
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的別名,用於指定發生錯誤時的回撥函式。
// 基本用法
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 處理 getJSON 和 前一個回撥函式執行時發生的錯誤
console.log('發生錯誤!', error);
});
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同於,
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
第二個then第一個引數null,只有rejected處理,
promise丟擲一個錯誤,就被catch方法指定的回撥函式捕獲。
// 寫法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 寫法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
reject方法的作用,等同於丟擲錯誤。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
Promise 在resolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為
** Promise 的狀態一旦改變,就永久保持該狀態**,
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
不會再變了。
Promise 物件的錯誤具有**“冒泡”性質**,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。 但是這種冒泡只有鏈條中可以處理
跟傳統的try/catch程式碼塊不同的是,如果沒有使用catch方法指定錯誤處理的回撥函式,Promise 物件丟擲的錯誤不會傳遞到外層程式碼,即不會有任何反應
Promise 內部的錯誤不會影響到 Promise 外部的程式碼,通俗的說法就是“Promise 會吃掉錯誤”。
Promise.prototype.finally()
finally方法用於指定不管 Promise 物件最後狀態如何,都會執行的操作。該方法是 ES2018 引入標準的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
不管promise最後的狀態,在執行完then或catch指定的回撥函式以後,都會執行finally方法指定的回撥函式。
finally方法的回撥函式不接受任何引數,這意味著沒有辦法知道,前面的 Promise 狀態到底是fulfilled還是rejected。這表明,
finally方法裡面的操作,應該是與狀態無關的,
不依賴於 Promise 的執行結果。
finally本質上是then方法的特例。
promise
.finally(() => {
// 語句
});
// 等同於
promise
.then(
result => {
// 語句
return result;
},
error => {
// 語句
throw error;
}
);
Promise.all()
Promise.all方法用於將多個 Promise 例項,包裝成一個新的 Promise 例項。
const p = Promise.all([p1, p2, p3]);
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物件的陣列
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
遍歷介面的好方法。。
如果作為引數的 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方法。
Promise.race()
同樣是將多個 Promise 例項,包裝成一個新的 Promise 例項。
只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
Promise.race方法的引數與Promise.all方法一樣,如果不是 Promise 例項,就會先呼叫下面講到的Promise.resolve方法,將引數轉為 Promise 例項,再進一步處理。
Promise.resolve()
將現有物件轉為 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')則是立即執行,因此最先輸出。