Promise是一個JavaScript標準內置對象。用來存儲一個異步任務的執行結果,以備未來使用。javascript
建立一個Promise對象:java
let promise = new Promise(function(resolve, reject) {
// executor
});
複製代碼
構造函數Promise接收一個函數(稱爲執行器executor)做爲參數,並向函數傳遞兩個函數做爲參數:resolve和reject。git
建立的Promise實例對象(如下用promise代替)具備如下內部屬性:github
當執行new Promise時,executor會當即執行。能夠在裏面書寫須要處理的異步任務,同步任務也支持。 任務處理完獲得的結果,須要調用如下回調之一:json
resolve/reject只須要一個參數(或不包含任何參數),多餘會被忽略。api
一個已經「settled」的promise(狀態已經變爲"fulfilled"或"rejected")將不能再次調用resolve或reject,即resolve或reject只能調用一次。剩下的resolve和reject的調用都會被忽略。數組
示例:promise
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done"), 1000);
});
let promise = new Promise(function(resolve, reject) {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
複製代碼
經過.then、.catch和.finally來消費promise。瀏覽器
promise的state和result屬性都是內部的,沒法直接訪問它們。 但咱們可使用 .then/.catch/.finally 來訪問。app
.then(f, f) 接收兩個函數參數: - 第一個函數在promise resolved後執行並接收結果做爲參數; - 第二個函數(非必填)在promise rejected後執行並接收error做爲參數;
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve("done!"), 1000);
});
promise.then(
result => alert(result), // 1 秒後顯示 "done!"
error => alert(error) // 不運行
);
複製代碼
.catch(f) 接收一個函數做爲參數,該函數接收reject的error做爲參數,是.then(null, f)的簡寫形式。
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("Whoops!")), 1000);
});
promise.catch(alert); // 1 秒後顯示 "Error: Whoops!"
複製代碼
.finally(f) 接收一個無參數的函數,不論promise被resolve或reject都會執行。
new Promise((resolve, reject) => {
// ...
}).finally(() => stop loading indicator)
.then(result => show result, err => show error);
複製代碼
finally會繼續將promise的結果傳遞下去。
.then/.catch/.finally異步執行:會等到promise的狀態由pending變爲settled時,當即執行。
更確切地說,當 .then/catch 處理程序應該執行時,它會首先進入內部隊列。JavaScript 引擎從隊列中提取處理程序,並在當前代碼完成時執行 setTimeout(..., 0)。
換句話說,.then(handler) 會被觸發,會執行相似於 setTimeout(handler, 0) 的動做。
在下述示例中,promise 被當即 resolved,所以 .then(alert) 被當即觸發:alert 會進入隊列,在代碼完成以後當即執行。
// 一個被當即 resolved 的 promise
let promise = new Promise(resolve => resolve("done!"));
promise.then(alert); // done!(在當前代碼完成以後)
alert("code finished"); // 這個 alert 會最早顯示
複製代碼
所以在 .then 以後的代碼老是在處理程序以前被執行(即便是在預先 resolved 的 promise 的狀況下)。
promise.then(f)的處理程序f(handler)調用後返回一個promise,handle自己返回的值會做爲這個promise的result;result能夠傳遞給下一個.then處理程序鏈進行傳遞。 對同一個promise分開.then時,每一次的結果都同樣,由於.then只是單純使用了promise提供的result,並不改變原來的promise自己。
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
複製代碼
.then(handler) 中所使用的處理程序(handler)能夠建立並返回一個 promise。 此時其餘的處理程序(handler)將等待它 settled 後再得到其result
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
複製代碼
handler也能夠返回一個「thenable」 對象 —— 一個具備方法 .then 的任意對象。它會被當作一個 promise 來對待。 第三方庫能夠實現本身的promise兼容對象。它們能夠具備擴展的方法集,但也與原生的 promise 兼容,由於它們實現了 .then 方法。
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// 1 秒後使用 this.num*2 進行 resolve
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // 1000ms 後顯示 2
複製代碼
handler返回的對象若是具備then方法,會被當即調用並接收resolve和reject做爲參數。直到resolve或reject執行後,再將result傳遞給下一個.then,以此類推,沿着鏈向下傳遞。
這個特性能夠用來將自定義的對象與 promise 鏈集成在一塊兒,而沒必要繼承自 Promise。 推薦異步行爲始終返回一個promise以便後續對鏈進行擴展。
若是 .then(或 catch)處理程序(handler)返回一個 promise,那麼鏈的其他部分將會等待,直到它狀態變爲 settled。當它被 settled 後,其 result(或 error)將被進一步傳遞下去。
比較promise.then(f1, f2);
和promise.then(f1).catch(f2);
Promise類有5種靜態方法:
這五個方法中,Promise.all 在實戰中使用的最多。
let promise = Promise.resolve(value)
—— 根據給定的 value 值返回 resolved promise。 等價於:let promise = new Promise(resolve => resolve(value));
let promise = Promise.reject(error)
—— 建立一個帶有 error 的 rejected promise。 等價於:let promise = new Promise((resolve, reject) => reject(error));
let promise = Promise.all([...promises...]);
—— 並行執行多個promise,返回一個新的promise,其結果爲包含全部promise的結果的有序數組。參數爲一個promise數組(嚴格能夠是任何可迭代對象,一般是數組)。
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 當 promise 就緒:每個 promise 即成爲數組中的一員
// 經常使用來發送並行請求
let names = ['iliakan', 'remy', 'jeresig'];
let requests = names.map(name => fetch(`https://api.github.com/users/${name}`));
Promise.all(requests)
.then(responses => {
// 全部響應都就緒時,咱們能夠顯示 HTTP 狀態碼
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // 每一個 url 都顯示 200
}
return responses;
})
// 映射 response 數組到 response.json() 中以讀取它們的內容
.then(responses => Promise.all(responses.map(r => r.json())))
// 全部 JSON 結果都被解析:「users」 是它們的數組
.then(users => users.forEach(user => alert(user.name)));
複製代碼
若是任意一個 promise爲reject,Promise.all返回的 promise 就會當即 reject 這個錯誤。並忽略全部列表中其餘的 promise。它們的結果也被忽略。
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).catch(alert); // Error: Whoops!
複製代碼
Promise.all(...) 接受可迭代的 promise 集合(大部分狀況下是數組)。可是若是這些對象中的任意一個不是 promise,它將會被直接包裝進 Promise.resolve。
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2, // 視爲 Promise.resolve(2)
3 // 視爲 Promise.resolve(3)
]).then(alert); // 1, 2, 3
複製代碼
用法同Promise.all,只不過Promise.allSettled會等待全部的 promise 都被處理:即便其中一個 reject,它仍然會等待其餘的 promise。處理完成後的數組由如下對象組成: {status:"fulfilled", value:result}
對於成功的響應, {status:"rejected", reason:error}
對於錯誤的響應。
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
Promise.allSettled(urls.map(url => fetch(url)))
.then(results => {
/* results: [ {status: 'fulfilled', value: ...response...}, {status: 'fulfilled', value: ...response...}, {status: 'rejected', reason: ...error object...} ]*/
results.forEach((result, num) => {
if (result.status == "fulfilled") {
alert(`${urls[num]}: ${result.value.status}`);
}
if (result.status == "rejected") {
alert(`${urls[num]}: ${result.reason}`);
}
});
});
複製代碼
若是瀏覽器不支持 Promise.allSettled,使用 polyfill 很容易讓其支持:
if(!Promise.allSettled) {
Promise.allSettled = function(promises) {
// p => Promise.resolve(p) 將該值轉換爲 promise(以防傳遞了非 promise)
return Promise.all(promises.map(p => Promise.resolve(p).then(
v => ({ state: 'fulfilled', value: v }),
r => ({ state: 'rejected', reason: r })
)));
};
}
複製代碼
let promise = Promise.race(iterable);
—— 與 Promise.all 相似,它接受一個可迭代的 promise 集合,可是隻要有一個promise被settled了就會中止等待,將這個promise的結果/錯誤做爲它的結果,其餘的promise的結果/錯誤都會被忽略。
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
複製代碼
try..catch
。// 1. 代碼執行錯誤
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!
// 1. 主動調用reject
new Promise((resolve, reject) => {
reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!
// 2. 處理器中的錯誤
new Promise((resolve, reject) => {
resolve("ok");
}).then((result) => {
throw new Error("Whoops!"); // rejects 這個 promise
}).then((result) => {
// 這個then不執行
}).catch(alert); // Error: Whoops!
// 3. 執行:catch -> then
new Promise((resolve, reject) => {
throw new Error("Whoops!");
}).catch(function(error) {
alert("The error is handled, continue normally");
}).then(() => alert("Next successful handler runs"));
複製代碼
若是promise變爲rejected,可是卻沒有catch處理這個錯誤,錯誤就被卡住(stuck),腳本就會死掉。
JavaScript引擎會跟蹤此類rejections,生成一個全局錯誤,能夠監聽unhandledrejection
事件捕獲。
window.addEventListener('unhandledrejection', function(event) {
// the event object has two special properties:
alert(event.promise); // [object Promise] - 產生錯誤的 promise
alert(event.reason); // Error: Whoops! - 未處理的錯誤對象
});
new Promise(function() {
throw new Error("Whoops!");
}); // 沒有 catch 處理錯誤
複製代碼
建議將.catch放在想要處理錯誤的位置,自定義錯誤類來幫助分析錯誤,還能夠從新拋出錯誤。 若是發生錯誤後沒法恢復腳本,那不用catch處理錯誤也行,可是應該使用unhandledrejection
事件來跟蹤錯誤。 使用finally處理必需要發生的任務,好比關閉loading。
有一個瀏覽器技巧是從 finally 返回零延時(zero-timeout)的 promise。這是由於一些瀏覽器(好比 Chrome)須要「一點時間」外的 promise 處理程序來繪製文檔的更改。所以它確保在進入鏈下一步以前,指示在視覺上是中止的。
在setTimeout中拋出的錯誤沒法被catch捕獲:
const promise = new Promise(function(resolve, reject) {
setTimeout(function () { throw new Error('test') }, 0)
});
promise.catch(function(error) { console.log(error) });
複製代碼
除非顯式調用reject:
const promise = new Promise(function(resolve, reject) {
setTimeout(function () {
reject(new Error('test'));
}, 0)
});
複製代碼
緣由:JS事件循環列表有宏任務與微任務之分:setTimeOut是宏任務, promise是微任務,他們有各自的執行順序;所以這段代碼的執行順序是:
hrow new Error('test')
此時這個異常實際上是在promise外部拋出的 但若是在setTimeOut中主動觸發了promise的reject方法,所以promise的catch將會在setTimeOut回調執行後的屬於他的微任務隊列中找到它而後執行,因此能夠捕獲錯誤Promise 的處理程序(handlers).then、.catch 和 .finally 都是異步的。 異步任務須要適當的管理。爲此,JavaScript 標準規定了一個內部隊列 PromiseJobs —— 「微任務隊列」(Microtasks queue)(v8 術語)。 這個隊列先進先出,只有引擎中沒有其餘任務運行時纔會啓動任務隊列的執行。 當一個 promise 準備就緒時,它的 .then/catch/finally 處理程序就被放入隊列中。等到當前代碼執行完而且以前排好隊的處理程序都完成時,JavaScript引擎會從隊列中獲取這些任務並執行。 即使一個 promise 當即被 resolve,.then、.catch 和 .finally 以後的代碼也會先執行。 若是要確保一段代碼在 .then/catch/finally 以後被執行,最好將它添加到 .then 的鏈式調用中。
let promise = Promise.resolve();
promise.then(() => alert("promise done"));
alert("code finished"); // 該警告框會首先彈出
複製代碼
指在 microtask 隊列結束時未處理的 promise 錯誤。 microtask隊列完成時,引擎會檢查promise,若是其中任何一個出現rejected狀態,就會觸發unhandledrejection事件。 但若是在setTimeout裏進行catch,unhandledrejection會先觸發,而後catch才執行,因此catch沒有發揮做用。
let promise = Promise.reject(new Error("Promise Failed!"));
setTimeout(() => promise.catch(err => alert('caught')));
window.addEventListener('unhandledrejection', event => alert(event.reason));
// Promise Failed! -> caught
複製代碼
簡單的示例
function loadScript(src, callback) {
let script = document.createElement('script');
script.src = src;
script.onload = () => callback(null, script);
script.onerror = () => callback(new Error(`Script load error for ${src}`));
document.head.append(script);
}
// promise改寫:
let loadScriptPromise = function(src) {
return new Promise((resolve, reject) => {
loadScript(src, (err, script) => {
if (err) reject(err)
else resolve(script);
});
})
}
// 用法:
// loadScriptPromise('path/script.js').then(...)
複製代碼
通用的promisify函數:
function promisify(f) {
return function (...args) { // 返回一個包裝函數
return new Promise((resolve, reject) => {
function callback(err, result) { // 給 f 用的自定義回調
if (err) {
return reject(err);
} else {
resolve(result);
}
}
args.push(callback); // 在參數的最後附上咱們自定義的回調函數
f.call(this, ...args); // 調用原來的函數
});
};
};
// 用法:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
複製代碼
以上回調函數只能接收兩個參數,接收多個參數的示例:
// 設定爲 promisify(f, true) 來獲取結果數組
function promisify(f, manyArgs = false) {
return function (...args) {
return new Promise((resolve, reject) => {
function callback(err, ...results) { // 給 f 用的自定義回調
if (err) {
return reject(err);
} else {
// 若是 manyArgs 被指定值,則 resolve 全部回調結果
resolve(manyArgs ? results : results[0]);
}
}
args.push(callback);
f.call(this, ...args);
});
};
};
// 用法:
f = promisify(f, true);
f(...).then(arrayOfResults => ..., err => ...)
複製代碼