當我們談論Promise時,我們說些什麼
各類詳細的 Promise
教程已經滿天飛了,我寫這一篇也只是用來自己用來總結和整理用的。如果有不足之處,歡迎指教。

為什麼我們要用Promise
JavaScript語言的一大特點就是單執行緒。單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。
為了解決單執行緒的堵塞問題,現在,我們的任務可以分為兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。
- 同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
- 非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。
非同步任務必須指定回撥函式,當主執行緒開始執行非同步任務,就是執行對應的回撥函式。而我們可能會寫出一個回撥金字塔,維護大量的callback將是一場災難:
step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // ... }); }); }); }); 複製程式碼
而Promise 可以讓非同步操作寫起來,就像在寫同步操作的流程,而不必一層層地巢狀回撥函式。
(new Promise(step1)) .then(step2) .then(step3) .then(step4); 複製程式碼
簡單實現一個Promise
關於 Promise
的學術定義和規範可以參考Promise/A+規範,中文版 ofollow,noindex">【翻譯】Promises/A+規範 。
Promise有三個狀態 pending
、 fulfilled
、 rejected
: 三種狀態切換隻能有兩種途徑,只能改變一次:
- 非同步操作從未完成(pending) => 成功(fulfilled)
- 非同步操作從未完成(pending) => 失敗(rejected)
Promise 例項的 then
方法,用來添加回調函式。
then
方法可以接受兩個回撥函式,第一個是非同步操作成功時(變為 fulfilled
狀態)時的回撥函式,第二個是非同步操作失敗(變為 rejected
)時的回撥函式(該引數可以省略)。一旦狀態改變,就呼叫相應的回撥函式。
下面是一個寫好註釋的簡單實現的Promise的實現:
class Promise { constructor(executor) { // 初始化state為pending this.state = 'pending' // 成功的值 this.value = undefined // 失敗的原因 this.reason = undefined // 非同步操作,我們需要將所有then中的成功呼叫儲存起來 this.onResolvedCallbacks = [] // 非同步操作,我們需要將所有then中的失敗呼叫儲存起來 this.onRejectedCallbacks = [] let resolve = value => { // 檢驗state狀態是否改變,如果改變了呼叫就會失敗 if (this.state === 'pending') { // resolve呼叫後,state轉化為成功態 this.state = 'fulfilled' // 儲存成功的值 this.value = value // 執行成功的回撥函式 this.onResolvedCallbacks.forEach(fn => fn) } } let reject = reason => { // 檢驗state狀態是否改變,如果改變了呼叫就會失敗 if (this.state === 'pending') { // reject呼叫後,state轉化為失敗態 this.state === 'rejected' // 儲存失敗的原因 this.reason = reason // 執行失敗的回撥函式 this.onRejectedCallbacks.forEach(fn => fn) } } // 如果executor執行報錯,直接執行reject try { executor(resolve, reject) } catch (err) { reject(err) } } // then 方法 有兩個引數onFulfilled onRejected then(onFulfilled, onRejected) { // 狀態為fulfilled,執行onFulfilled,傳入成功的值 if (this.state === 'fulfilled') { onFulfilled(this.value) } // 狀態為rejected,執行onRejected,傳入失敗的原因 if (this.state === 'rejected') { onRejected(this.reason) } // 當狀態state為pending時 if (this.state === 'pending') { // onFulfilled傳入到成功陣列 this.onResolvedCallbacks.push(()=>{ onFulfilled(this.value); }) // onRejected傳入到失敗陣列 this.onRejectedCallbacks.push(()=>{ onRejected(this.reason); }) } } } 複製程式碼
如果需要實現鏈式呼叫和其它API,請檢視下面參考文件連結中的手寫Promise教程。
優雅的使用Promise
使用Promise封裝一個HTTP請求
function get(url) { return new Promise(function(resolve, reject) { var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { if (req.status == 200) { resolve(req.responseText); } else { 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); }) // 當前收到的是純文字,但我們需要的是JSON物件。我們將該方法修改一下 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); } 複製程式碼
then()
不是Promise的最終部分,可以將各個 then
連結在一起來改變值,或依次執行額外的非同步操作。
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); }) 複製程式碼
錯誤處理
then()
包含兩個引數 onFulfilled
, onRejected
。 onRejected
是失敗時呼叫的函式。
對於失敗,我們還可以使用 catch
,對於錯誤進行捕捉,但下面兩段程式碼是有差異的:
get('story.json').then(function(response) { console.log("Success!", response); }, function(error) { console.log("Failed!", error); }) 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
拒絕時兩者均被呼叫,因為它們在該鏈中是單獨的步驟。看看下面的程式碼:
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!"); }) 複製程式碼
以下是上述程式碼的流程圖形式:

藍線表示執行的 promise 路徑,紅路表示拒絕的 promise 路徑。與 JavaScript 的 try/catch 一樣,錯誤被捕獲而後續程式碼繼續執行。
並行和順序:兩者兼得
假設我們獲取了一個 story.json
檔案,其中包含了文章的標題,和段落的下載地址。
1. 順序下載,依次處理
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); return story.chapterUrls.reduce(function(sequence, chapterUrl) { // Once the last chapter's promise is done… return sequence.then(function() { // …fetch the next chapter return getJSON(chapterUrl); }).then(function(chapter) { // and add it to the page addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { // And we're all done! addTextToPage("All done"); }).catch(function(err) { // Catch any error that happened along the way addTextToPage("Argh, broken: " + err.message); }).then(function() { // Always hide the spinner document.querySelector('.spinner').style.display = 'none'; }) 複製程式碼
2. 並行下載,完成後統一處理
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // Take an array of promises and wait on them all return Promise.all( // Map our array of chapter urls to // an array of chapter json promises story.chapterUrls.map(getJSON) ); }).then(function(chapters) { // Now we have the chapters jsons in order! Loop through… chapters.forEach(function(chapter) { // …and add to the page addHtmlToPage(chapter.html); }); addTextToPage("All done"); }).catch(function(err) { addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; }) 複製程式碼
3. 並行下載,一旦順序正確立即渲染
getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // Map our array of chapter urls to // an array of chapter json promises. // This makes sure they all download parallel. return story.chapterUrls.map(getJSON) .reduce(function(sequence, chapterPromise) { // Use reduce to chain the promises together, // adding content to the page for each chapter return sequence.then(function() { // Wait for everything in the sequence so far, // then wait for this chapter to arrive. return chapterPromise; }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { addTextToPage("All done"); }).catch(function(err) { // catch any error that happened along the way addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; }) 複製程式碼
async / await
async
函式返回一個 Promise 物件,可以使用 then
方法添加回調函式。當函式執行的時候,一旦遇到 await
就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。
基本用法
我們可以重寫一下之前的 getJSON
方法:
// promise 寫法 function getJSON(url) { return get(url).then(JSON.parse).catch(err => { console.log('getJSON failed for', url, err); throw err; }) } // async 寫法 async function getJSON(url) { try { let response = await get(url) return JSON.parse(response) } catch (err) { console.log('getJSON failed for', url, err); } } 複製程式碼
注意:避免太過迴圈
假定我們想獲取一系列段落,並儘快按正確順序將它們列印:
// promise 寫法 function chapterInOrder(urls) { return urls.map(getJSON) .reduce(function(sequence, chapterPromise) { return sequence.then(function() { return chapterPromise; }).then(function(chapter) { console.log(chapter) }); }, Promise.resolve()) } 複製程式碼
* 不推薦的方式:
async function chapterInOrder(urls) { for (const url of urls) { const chapterPromise = await getJSON(url); console.log(chapterPromise); } } 複製程式碼
推薦寫法:
async function chapterInOrder(urls) { const chapters = urls.map(getJSON); // log them in sequence for (const chapter of chapters) { console.log(await chapter); } } 複製程式碼