JavaScript 是單線程的,這意味着任何兩句代碼都不能同時運行,它們得一個接一個來。在瀏覽器中,JavaScript 和其餘任務共享一個線程,不一樣的瀏覽器略有差別,但大致上這些和 JavaScript 共享線程的任務包括重繪、更新樣式、用戶交互等,全部這些任務操做都會阻塞其餘任務。javascript
1、事件的不足php
對於那些執行時間很長,而且長時間佔用線程的代碼,咱們一般使用異步來執行,可是又如何判斷其是否執行完畢或者失敗呢?咱們一般使用事件監聽,但事件監聽只能監聽綁定以後發生的事件,但有可能你寫綁定事件代碼以前該事件就已經發生,這樣你就沒法檢測。下面進行說明:html
你應該會用事件加回調的辦法來處理這類狀況:html5
var img1 = document.querySelector('.img-1'); img1.addEventListener('load', function() { // 啊哈圖片加載完成 }); img1.addEventListener('error', function() { // 哎喲出問題了 });
這樣加載圖片就不會佔據線程。咱們添加幾個監聽函數,請求圖片,而後 JavaScript 就中止運行了,直到觸發某個監聽函數。java
上面的例子中惟一的問題是,事件有可能在咱們綁定監聽器以前就已經發生,因此咱們先要檢查圖片的「complete」屬性:git
var img1 = document.querySelector('.img-1'); function loaded() { // 啊哈圖片加載完成 } if (img1.complete) { loaded(); } else { img1.addEventListener('load', loaded); } img1.addEventListener('error', function() { // 哎喲出問題了 });
這樣還不夠,若是在添加監聽函數以前圖片加載發生錯誤,咱們的監聽函數仍是白費,不幸的是 DOM 也沒有爲這個需求提供解決辦法。並且,這還只是處理一張圖片的狀況,若是有一堆圖片要處理那就更麻煩了。es6
事件機制最適合處理同一個對象上反覆發生的事情—— keyup、touchstart 等等。你不須要考慮當綁定監聽器以前所發生的事情,當碰到異步請求成功/失敗的時候,你想要的一般是這樣:github
img1.callThisIfLoadedOrWhenLoaded(function() { // 加載完成 }).orIfFailedCallThis(function() { // 加載失敗 }); // 以及…… whenAllTheseHaveLoaded([img1, img2]).callThis(function() { // 所有加載完成 }).orIfSomeFailedCallThis(function() { // 一個或多個加載失敗 });
這就是 Promise。若是 HTML 圖片元素有一個「ready()」方法的話,咱們就能夠這樣:web
img1.ready().then(function() { // 加載完成 }, function() { // 加載失敗 }); // 以及…… Promise.all([img1.ready(), img2.ready()]).then(function() { // 所有加載完成 }, function() { // 一個或多個加載失敗 });
基本上 Promise 仍是有點像事件回調的,除了:ajax
這些特性很是適合處理異步操做的成功/失敗情景,你無需再擔憂事件發生的時間點,而只需對其作出響應。
2、promise使回調函數和異步操做完全分離
看了上述所講,感受promise和回調函數做用差很少,但對於多層嵌套的回調,在代碼組織上確實優雅不少。
網頁的交互愈來愈複雜,JavaScript 的異步操做也隨之愈來愈多。如常見的 ajax 請求,須要在請求完成時響應操做,請求一般是異步的,請求的過程當中用戶還能進行其餘的操做,不會對頁面進行阻塞,這種異步的交互效果對用戶來講是挺有友好的。可是對於開發者來講,要大量處理這種操做,就很不友好了。異步請求完成的操做必須預先定義在回調函數中,等到請求完成就必須調用這個函數。這種非線性的異步編程方式會讓開發者很不適應,同時也帶來了諸多的不便,增長了代碼的耦合度和複雜性,代碼的組織上也會很不優雅,大大下降了代碼的可維護性。狀況再複雜點,若是一個操做要等到多個異步 ajax 請求的完成才能進行,就會出現回調函數嵌套的狀況,若是須要嵌套好幾層,那你就只能自求多福了。
先看看下面這個常見的異步函數。
var showMsg = function(){
setTimeout(function(){
alert( ‘hello’ );
}, 5000 );
};
若是要給該函數添加回調,一般會這麼幹。
var showMsg = function( callback ){
setTimeout(function(){
alert( ‘hello’ );
// 此處添加回調
callback();
}, 5000 );
};
若是是使用 easy.js 的 Promise,添加回調的方法就會優雅多了,前提是須要將原函數封裝成一個 promise 實例。
var showMsg = function(){
// 構造promise實例
var promise = new E.Promise();
setTimeout(function(){
alert( ‘hello’ );
// 改變promise的狀態
promise.resolve( ‘done’ );
}, 5000 );
// 返回promise實例
return promise;
};
將一個普通的函數封裝成一個 promise 實例,有3個關鍵步驟,第一步是在函數內部構造一個 promise 實例,第二步是部署函數執行完去改變 promise 的狀態爲已完成,第三步就是返回這個 promise 實例。每一個 promise 實例都有3種狀態,分別爲 pending(未完成)、resolved(已完成,成功)、rejected(已拒絕,失敗)。下面再來看看如何添加回調。
showMsg().then(function( str ){
// 回調添加到這裏來了
callback( str );
});
這樣就將回調函數和原來的異步函數完全的分離了,從代碼組織上看,優雅了不少。resolve 接受一個參數,該參數就能夠輕鬆實現將數據傳送給使用 then 方法添加的回調中。
對於 ajax 請求,easy.js 直接將 ajax 方法封裝成了 promise 對象,能夠直接添加 then 方法來回調。
E.ajax({
url : ‘test1.php’,
type : ‘GET’
})
then(function(){
// 添加請求成功的回調
}, function(){
// 添加請求失敗的回調
});
then 方法接受2個函數做爲參數,第一個函數是已完成的回調,第二個就是已失敗的回調。
若是有上面提到的多個 ajax 請求的狀況呢?那麼就要用到 when 這個方法了。該方法能夠接受多個 promise 實例做爲參數。
var requests = E.when(E.ajax({
url : ‘test1.php’,
type : ‘GET’
}), E.ajax({
url : ‘test2.php’,
type : ‘GET’
}));
requests.then(function( arg1, arg2 ){
console.log( ‘success:’ + arg1[0] + arg2[0] );
}, function( arg1, arg2 ){
console.log( ‘failure:’ + arg1 + arg2 );
});
when 方法是將多個 promise 實例存到一個數組中,等到該數組的全部 promise 實例都是已完成狀態纔去執行已完成的回調,一旦有一個實例是已拒絕的狀態,則當即執行已拒絕的回調。
3、promise中你可能不知道的事情
1.then()返回promise
// Exhibit A var p = new Promise(/*...*/); p.then(func1); p.then(func2);
// Exhibit B var p = new Promise(/*...*/); p.then(func1) .then(func2);
若func1執行錯誤,A狀況下,func2會正常執行,但B狀況下,func2不會執行
2.then()中的回調函數必須返回參數
3.只有上一層錯誤才能被拋出
// Exhibit A new Promise(function(resolve, reject) { resolve("hello world"); }) .then( function(str) { throw new Error("uh oh"); }, undefined ) .then( undefined, function(error) { alert(error); } );
// Exhibit B new Promise(function(resolve, reject) { resolve("hello world"); }) .then( function(str) { throw new Error("uh oh"); }, function(error) { alert(error); } );
A狀況下錯誤會拋出,B狀況下不會
4.錯誤若是沒有再次拋出,將被視做已經修復,會繼續執行then()
var p = new Promise(function(resolve, reject) { reject(new Error("pebkac")); }); p.then( undefined, function(error) { } ) .then( function(str) { alert("I am saved!"); }, function(error) { alert("Bad computer!"); } );
5.promises也能夠被中途終止
var p = new Promise(/*...*/); p.then(function(str) { if(!loggedIn) { return new Promise(/*...*/); } }) .then(function(str) { alert("Done."); })
只要在then()中加入return new Promise(/*...*/);
6.promise中的resolve()函數不是馬上執行的
function runme() { var i = 0; new Promise(function(resolve) { resolve(); }) .then(function() { i += 2; }); alert(i); }
上述代碼執行結果不必定是2,由於你以爲resolve是同步的,會馬上執行。可是你錯了!promise規定全部調用都必須是異步,因此當執行到alert(i)時,i可能還沒被修改!
4、應用
OK,如今咱們來寫點實際的代碼。假設咱們想要:
……這個過程當中若是發生什麼錯誤了要通知用戶,而且把加載指示停掉,否則它就會不停轉下去,使人眼暈,或者搞壞界面什麼的。
固然了,你不會用 JavaScript 去這麼繁瑣地顯示一篇文章,直接輸出 HTML 要快得多,不過這個流程是很是典型的 API 請求模式:獲取多個數據,當它們所有完成以後再作一些事情。
首先,搞定從網絡加載數據的步驟:
只要能保持向後兼容,現有 API 都會更新以支持 Promise,XMLHttpRequest
是重點考慮對象之一。不過如今咱們先來寫個 GET 請求:
function get(url) { // 返回一個新的 Promise return new Promise(function(resolve, reject) { // 經典 XHR 操做 var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { // 當發生 404 等情況的時候調用此函數 // 因此先檢查狀態碼 if (req.status == 200) { // 以響應文本爲結果,完成此 Promise resolve(req.response); } else { // 不然就以狀態碼爲結果否認掉此 Promise // (提供一個有意義的 Error 對象) reject(Error(req.statusText)); } }; // 網絡異常的處理方法 req.onerror = function() { reject(Error("Network Error")); }; // 發出請求 req.send(); }); }
如今能夠調用它了:
get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.error("Failed!", error); });
鏈式調用
「then」的故事還沒完,你能夠把這些「then」串聯起來修改結果或者添加進行更多異步操做。
你能夠對結果作些修改而後返回一個新值:
var promise = new Promise(function(resolve, reject) { resolve(1); }); promise.then(function(val) { console.log(val); // 1 return val + 2; }).then(function(val) { console.log(val); // 3 });
回到前面的代碼:
get('story.json').then(function(response) { console.log("Success!", response); });
收到的響應是一個純文本的 JSON,咱們能夠修改 get 函數,設置 responseType
爲 JSON 來指定服務器響應格式,也能夠在 Promise 的世界裏搞定這個問題:
get('story.json').then(function(response) { return JSON.parse(response); }).then(function(response) { console.log("Yey JSON!", response); });
既然 JSON.parse
只接收一個參數,並返回轉換後的結果,咱們還能夠再精簡一下:
get('story.json').then(JSON.parse).then(function(response) { console.log("Yey JSON!", response); });
事實上,咱們能夠把getJSON
函數寫得超級簡單:
function getJSON(url) { return get(url).then(JSON.parse); }
getJSON
會返回一個獲取 JSON 並加以解析的 Promise。
你也能夠把「then」串聯起來依次執行異步操做。
當你從「then」的回調函數返回的時候,這裏有點小魔法。若是你返回一個值,它就會被傳給下一個「then」的回調;而若是你返回一個「類 Promise」的對象,則下一個「then」就會等待這個 Promise 明確結束(成功/失敗)纔會執行。例如:
getJSON('story.json').then(function(story) { return getJSON(story.chapterUrls[0]); }).then(function(chapter1) { console.log("Got chapter 1!", chapter1); });
這裏咱們發起一個對「story.json」的異步請求,返回給咱們更多 URL,而後咱們會請求其中的第一個。Promise 開始首次顯現出相較事件回調的優越性了。你甚至能夠寫一個抓取章節內容的獨立函數:
var storyPromise; function getChapter(i) { storyPromise = storyPromise || getJSON('story.json'); return storyPromise.then(function(story) { return getJSON(story.chapterUrls[i]); }) } // 用起來很是簡單: getChapter(0).then(function(chapter) { console.log(chapter); return getChapter(1); }).then(function(chapter) { console.log(chapter); });
咱們一開始並不加載 story.json,直到第一次 getChapter
,而之後每次 getChapter
的時候均可以重用已經加載完成的 story Promise,因此 story.json 只須要請求一次。Promise 好棒!
前面已經看到,「then」接受兩個參數,一個處理成功,一個處理失敗(或者說確定和否認,按 Promise 術語):
get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.log("Failed!", error); });
你還可使用「catch」:
get('story.json').then(function(response) { console.log("Success!", response); }).catch(function(error) { console.log("Failed!", error); });
這裏的 catch 並沒有任何特殊之處,只是 比then(undefined, func)
的語法更直觀一點而已。注意上面兩段代碼的行爲不只相同,後者至關於:
get('story.json').then(function(response) { console.log("Success!", response); }).then(undefined, function(error) { console.log("Failed!", error); });
差別不大,但意義非凡。Promise 被否認以後會跳轉到以後第一個配置了否認回調的 then(或 catch,同樣的)。對於 then(func1, func2)
來講,必會調用 func1
或func2
之一,但毫不會兩個都調用。而 then(func1).catch(func2)
這樣,若是 func1
返回否認的話 func2
也會被調用,由於他們是鏈式調用中獨立的兩個步驟。看下面這段代碼:
asyncThing1().then(function() { return asyncThing2(); }).then(function() { return asyncThing3(); }).catch(function(err) { return asyncRecovery1(); }).then(function() { return asyncThing4(); }, function(err) { return asyncRecovery2(); }).catch(function(err) { console.log("Don't worry about it"); }).then(function() { console.log("All done!"); });
這段流程很是像 JavaScript 的 try/catch 組合,「try」代碼塊中發生的錯誤會當即跳轉到「catch」代碼塊。這是上面那段代碼的流程圖(我最愛流程圖了):
綠線是確定的 Promise 流程,紅線是否認的 Promise 流程。
Promise 的否認回調能夠由 Promise.reject() 觸發,也能夠由構造器回調中拋出的錯誤觸發:
var jsonPromise = new Promise(function(resolve, reject) { // 若是數據格式不對的話 JSON.parse 會拋出錯誤 // 能夠做爲隱性的否認結果: resolve(JSON.parse("This ain't JSON")); }); jsonPromise.then(function(data) { // 永遠不會發生: console.log("It worked!", data); }).catch(function(err) { // 這纔是真相: console.log("It failed!", err); });
這意味着你能夠把全部 Promise 相關操做都放在它的構造函數回調中進行,這樣發生任何錯誤都能捕捉到而且觸發 Promise 否認。
「then」回調中拋出的錯誤也同樣:
get('/').then(JSON.parse).then(function() { // This never happens, '/' is an HTML page, not JSON // so JSON.parse throws console.log("It worked!", data); }).catch(function(err) { // Instead, this happens: console.log("It failed!", err); });
回到咱們的故事和章節,咱們用 catch
來捕捉錯誤並顯示給用戶:
getJSON('story.json').then(function(story) { return getJSON(story.chapterUrls[0]); }).then(function(chapter1) { addHtmlToPage(chapter1.html); }).catch(function() { addTextToPage("Failed to show chapter"); }).then(function() { document.querySelector('.spinner').style.display = 'none'; });
若是請求 story.chapterUrls[0]
失敗(http 500 或者用戶掉線什麼的)了,它會跳過以後全部針對成功的回調,包括 getJSON
中將響應解析爲 JSON 的回調,和這裏把第一張內容添加到頁面裏的回調。JavaScript 的執行會進入 catch 回調。結果就是前面任何章節請求出錯,頁面上都會顯示「Failed to show chapter」。
和 JavaScript 的 try/catch 同樣,捕捉到錯誤以後,接下來的代碼會繼續執行,按計劃把加載指示器給停掉。上面的代碼就是下面這段的非阻塞異步版:
try { var story = getJSONSync('story.json'); var chapter1 = getJSONSync(story.chapterUrls[0]); addHtmlToPage(chapter1.html); } catch (e) { addTextToPage("Failed to show chapter"); } document.querySelector('.spinner').style.display = 'none';
若是隻是要捕捉異常作記錄輸出而不打算在用戶界面上對錯誤進行反饋的話,只要拋出 Error 就好了,這一步能夠放在 getJSON
中:
function getJSON(url) { return get(url).then(JSON.parse).catch(function(err) { console.log("getJSON failed for", url, err); throw err; }); }
如今咱們已經搞定第一章了,接下來搞定所有章節。
異步的思惟方式並不符合直覺,若是你以爲起步困難,那就試試先寫個同步的方法,就像這個:
try { var story = getJSONSync('story.json'); addHtmlToPage(story.heading); story.chapterUrls.forEach(function(chapterUrl) { var chapter = getJSONSync(chapterUrl); addHtmlToPage(chapter.html); }); addTextToPage("All done"); } catch (err) { addTextToPage("Argh, broken: " + err.message); } document.querySelector('.spinner').style.display = 'none';
它執行起來徹底正常!不過它是同步的,在加載內容時會卡住整個瀏覽器。要讓它異步工做的話,咱們用 then 把它們一個接一個串起來:
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // TODO: 獲取並顯示 story.chapterUrls 中的每一個 url }).then(function() { // 所有完成啦! addTextToPage("All done"); }).catch(function(err) { // 若是過程當中有任何不對勁的地方 addTextToPage("Argh, broken: " + err.message); }).then(function() { // 不管如何要把 spinner 隱藏掉 document.querySelector('.spinner').style.display = 'none'; });
那麼咱們如何遍歷章節的 URL 而且依次請求?這樣是不行的:
story.chapterUrls.forEach(function(chapterUrl) { // Fetch chapter getJSON(chapterUrl).then(function(chapter) { // and add it to the page addHtmlToPage(chapter.html); }); });
「forEach」 沒有對異步操做的支持,因此咱們的故事章節會按照它們加載完成的順序顯示,基本上《低俗小說》就是這麼寫出來的。咱們不寫低俗小說,因此得修正它:
咱們要把章節 URL 數組轉換成 Promise 的序列,仍是用 then
:
// 從一個完成狀態的 Promise 開始 var sequence = Promise.resolve(); // 遍歷全部章節的 url story.chapterUrls.forEach(function(chapterUrl) { // 從 sequence 開始把操做接龍起來 sequence = sequence.then(function() { return getJSON(chapterUrl); }).then(function(chapter) { addHtmlToPage(chapter.html); }); });
這是咱們第一次用到 Promise.resolve
,它會依據你傳的任何值返回一個 Promise。若是你傳給它一個類 Promise 對象(帶有 then
方法),它會生成一個帶有一樣確定/否認回調的 Promise,基本上就是克隆。若是傳給它任何別的值,如Promise.resolve('Hello')
,它會建立一個以這個值爲完成結果的 Promise,若是不傳任何值,則以 undefined 爲完成結果。
還有一個對應的 Promise.reject(val)
,會建立以你傳入的參數(或 undefined)爲否認結果的 Promise。
咱們能夠用 array.reduce
精簡一下上面的代碼:
// 遍歷全部章節的 url story.chapterUrls.reduce(function(sequence, chapterUrl) { // 從 sequence 開始把操做接龍起來 return sequence.then(function() { return getJSON(chapterUrl); }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve());
它和前面的例子功能同樣,可是不須要顯式聲明 sequence
變量。reduce
回調會依次應用在每一個數組元素上,第一輪裏的「sequence」是 Promise.resolve()
,以後的調用裏「sequence」就是上次函數執行的的結果。array.reduce
很是適合用於把一組值歸併處理爲一個值,正是咱們如今對 Promise 的用法。
彙總上面的代碼:
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); return story.chapterUrls.reduce(function(sequence, chapterUrl) { // 當前一個章節的 Promise 完成以後…… return sequence.then(function() { // ……獲取下一章 return getJSON(chapterUrl); }).then(function(chapter) { // 並添加到頁面 addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { // 如今所有完成了! addTextToPage("All done"); }).catch(function(err) { // 若是過程當中發生任何錯誤 addTextToPage("Argh, broken: " + err.message); }).then(function() { // 保證 spinner 最終會隱藏 document.querySelector('.spinner').style.display = 'none'; });
查看代碼運行示例,前面的同步代碼改形成了徹底異步的版本。咱們還能夠更進一步。如今頁面加載的效果是這樣:
瀏覽器很擅長同時加載多個文件,咱們這種一個接一個下載章節的方法很是低效率。咱們但願同時下載全部章節,所有完成後一次搞定,正好就有這麼個 API:
Promise.all(arrayOfPromises).then(function(arrayOfResults) { //... });
Promise.all
接受一個 Promise 數組爲參數,建立一個當全部 Promise 都完成以後就完成的 Promise,它的完成結果是一個數組,包含了全部先前傳入的那些 Promise 的完成結果,順序和將它們傳入的數組順序一致。
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // 接受一個 Promise 數組並等待他們所有完成 return Promise.all( // 把章節 URL 數組轉換成對應的 Promise 數組 story.chapterUrls.map(getJSON) ); }).then(function(chapters) { // 如今咱們有了順序的章節 JSON,遍歷它們…… chapters.forEach(function(chapter) { // ……並添加到頁面中 addHtmlToPage(chapter.html); }); addTextToPage("All done"); }).catch(function(err) { // 捕獲過程當中的任何錯誤 addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; });
根據鏈接情況,改進的代碼會比順序加載方式提速數秒(查看示例),甚至代碼行數也更少。章節加載完成的順序不肯定,但它們顯示在頁面上的順序準確無誤。
然而這樣仍是有提升空間。當第一章內容加載完畢咱們能夠當即填進頁面,這樣用戶能夠在其餘加載任務還沒有完成以前就開始閱讀;當第三章到達的時候咱們不動聲色,第二章也到達以後咱們再把第二章和第三章內容填入頁面,以此類推。
爲了達到這樣的效果,咱們同時請求全部的章節內容,而後建立一個序列依次將其填入頁面:
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // 把章節 URL 數組轉換成對應的 Promise 數組 // 這樣就能夠並行加載它們 return story.chapterUrls.map(getJSON) .reduce(function(sequence, chapterPromise) { // 使用 reduce 把這些 Promise 接龍 // 以及將章節內容添加到頁面 return sequence.then(function() { // 等待當前 sequence 中全部章節和本章節的數據到達 return chapterPromise; }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { addTextToPage("All done"); }).catch(function(err) { // 捕獲過程當中的任何錯誤 addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; });
魚與熊掌兼得!加載全部內容的時間未變,但用戶能夠更早看到第一章。
這個小例子中各部分章節加載差很少同時完成,逐章顯示的策略在章節內容不少的時候優點將會更加顯著。
上面的代碼若是用 Node.js 風格的回調或者事件機制實現的話代碼量大約要翻一倍,更重要的是可讀性也不如此例。然而,Promise 的厲害不止於此,和其餘 ES6 的新功能結合起來還能更加高效……
除非額外註明,Chrome、Opera 和 Firefox(nightly)均支持下列全部方法。這個 polyfill 則在全部瀏覽器內實現了一樣的接口。
Promise.resolve(promise);
promise.constructor == Promise
)
Promise.resolve(thenable);
Promise.resolve(obj);
Promise.reject(obj);
Promise.all(array);
Promise.race(array);
備註:我不大肯定這個接口是否有用,我更傾向於一個 Promise.all
的對立方法,僅當全部數組元素所有給出否認的時候才拋出否認結果
new Promise(function(resolve, reject) {});
resolve(thenable)
resolve(obj)
obj
做爲確定結果完成
reject(obj)
obj
做爲否認結果完成。出於一致性和調試(如棧追蹤)方便,obj
應該是一個 Error 對象的實例。構造器的回調函數中拋出的錯誤會被當即傳遞給 reject()
。
promise.then(onFulfilled, onRejected)
onFulfilled
。 當 promise 以否認結束時會調用onRejected
。 這兩個參數都是可選的,當任意一個未定義時,對它的調用會跳轉到 then 鏈的下一個 onFulfilled
/onRejected
上。 這兩個回調函數均只接受一個參數,確定結果或者否認緣由。 當 Promise.resolve
確定結束以後,then
會返回一個新的 Promise,這個 Promise 至關於你從 onFulfilled
/onRejected
中返回的值。若是回調中拋出任何錯誤,返回的 Promise 也會以此錯誤做爲否認結果結束。
promise.catch(onRejected)
promise.then(undefined, onRejected)
的語法糖。