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 來訪問。markdown
.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 => ...) 複製代碼