JavaScript Promise:簡介

Promise 可簡化延遲和非同步運算。Promise 代表尚未完成的作業。

Jake Archibald
Jake Archibald

開發人員,準備好迎接網頁開發史上最重要的時刻。

[Drumroll begins]

JavaScript 支援 Promise 了!

[Fireworks explode, glittery paper rains from above, the crowd goes wild]

此時,您會屬於下列其中一個類別:

  • 周圍的人都在歡呼,但你不知道發生了什麼事。你甚至可能不確定「承諾」是什麼。你聳聳肩,但閃亮紙張的重量壓在你的肩上。如果沒有,也別擔心,我花了很長的時間才弄清楚為什麼應該重視這些東西。建議您先從開頭開始。
  • 你揮拳慶祝!這項功能終於推出了,您之前使用過這些 Promise 項目,但所有實作項目都有稍微不同的 API,這讓您感到困擾。官方 JavaScript 版本的 API 是什麼?建議您先瞭解術語
  • 你早就知道這件事,而且對於那些像發現新大陸一樣興奮的人嗤之以鼻。請先享受一下自己的優越感,然後直接前往 API 參考資料

瀏覽器支援和 Polyfill

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

如要讓缺少完整 Promise 實作的瀏覽器符合規格,或在其他瀏覽器和 Node.js 中新增 Promise,請查看這個 Polyfill (2k gzipped)。

這有什麼好大驚小怪的?

JavaScript 是單一執行緒,表示兩段指令碼無法同時執行,必須依序執行。在瀏覽器中,JavaScript 會與大量其他項目共用執行緒,這些項目因瀏覽器而異。但通常 JavaScript 會與繪製、更新樣式及處理使用者動作 (例如醒目顯示文字和與表單控制項互動) 位於同一個佇列。其中一項活動會延遲其他活動。

身為人類,你具有多執行緒能力。你可以用多根手指打字,同時開車和進行對話。我們唯一需要處理的封鎖函式是打噴嚏,因為打噴嚏時必須暫停所有目前的活動。這相當惱人,尤其是在開車時想與人交談時。您不會想編寫容易出錯的程式碼。

您可能已使用事件和回呼來解決這個問題。以下是相關事件:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

這完全不會讓人想打噴嚏。我們取得圖片、新增幾個監聽器,然後 JavaScript 可以停止執行,直到其中一個監聽器遭到呼叫為止。

很遺憾,在上述範例中,事件可能發生在我們開始監聽事件之前,因此我們需要使用圖片的「complete」屬性來解決這個問題:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

這不會擷取我們有機會監聽之前發生錯誤的圖片;很遺憾,DOM 無法提供這類做法。此外,這項作業會載入一張圖片。如果想知道一組圖片何時載入,情況會更加複雜。

事件不一定是最佳做法

事件非常適合用於在同一物件上發生多次的動作,例如 keyuptouchstart 等。對於這些事件,您其實不在意附加監聽器之前發生了什麼事。但如果是非同步成功/失敗,理想情況下您會希望看到類似下列內容:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

這就是 Promise 的用途,但命名方式更完善。如果 HTML 圖片元素有傳回 Promise 的「ready」方法,我們就可以執行下列操作:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

從最基本的角度來看,Promise 有點類似事件監聽器,但有以下例外狀況:

  • Promise 只能成功或失敗一次。該作業不會成功或失敗兩次,也不會從成功切換為失敗,反之亦然。
  • 如果 Promise 成功或失敗,而您稍後新增成功/失敗回呼,即使事件發生在稍早,系統仍會呼叫正確的回呼。

這對非同步成功/失敗來說非常實用,因為您對某個項目何時可供使用不太感興趣,而是對結果的反應更感興趣。

Promise 術語

Domenic Denicola 校對了本文初稿,並因術語問題給了我「F」的成績。他讓我留校察看,強迫我抄寫 100 次《States and Fates》,還寫信給我的父母,表示很擔心我。儘管如此,我還是經常搞混這些術語,但以下是基本概念:

承諾可以是:

  • fulfilled - 與承諾相關的動作已成功完成
  • rejected - The action relating to the promise failed
  • 待處理:尚未完成或拒絕
  • 已結算 - 已完成或拒絕

規格也使用「thenable」一詞來描述類似 Promise 的物件,因為這類物件具有 then 方法。這個詞讓我想起前英格蘭足球隊總教練 Terry Venables,因此我會盡量少用。

JavaScript 支援 Promise!

Promise 已存在一段時間,並以程式庫的形式提供,例如:

上述 Promise 和 JavaScript Promise 具有共同的標準化行為,稱為 Promises/A+。如果您是 jQuery 使用者,則有類似的項目,稱為「延遲」。不過,Deferreds 不符合 Promise/A+ 規範,因此略有不同且實用性較低,請多加留意。jQuery 也有 Promise 型別,但這只是 Deferred 的子集,且有相同問題。

雖然 Promise 實作遵循標準化行為,但整體 API 不同。JavaScript Promise 的 API 與 RSVP.js 類似。建立 Promise 的方法如下:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Promise 建構函式會採用一個引數,也就是含有兩個參數 (resolve 和 reject) 的回呼。在回呼中執行某些動作 (可能是非同步),然後在一切順利時呼叫 resolve,否則呼叫 reject。

如同舊版 JavaScript 中的 throw,以 Error 物件拒絕是慣例,但並非必要。錯誤物件的優點是會擷取堆疊追蹤記錄,讓偵錯工具更加實用。

使用該 Promise 的方式如下:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() 會採用兩個引數,一個是成功案例的回呼,另一個是失敗案例的回呼。這兩者都是選用項目,因此您可以只為成功或失敗案例新增回呼。

JavaScript Promise 最初是 DOM 中的「Futures」,後來重新命名為「Promises」, 最後移至 JavaScript。將這些 API 放在 JavaScript 中而非 DOM 中,好處是可在非瀏覽器 JS 環境 (例如 Node.js) 中使用 (是否在核心 API 中使用則是另一個問題)。

雖然 DOM 是 JavaScript 功能,但仍可放心使用。事實上,所有具有非同步成功/失敗方法的新 DOM API 都會使用 Promise。這項措施已在配額管理字型載入事件ServiceWorkerWeb MIDIStreams 等功能中實施。

與其他程式庫的相容性

JavaScript Promise API 會將任何具有 then() 方法的項目視為類似 Promise (或 Promise 說法中的 thenable sigh),因此如果您使用會傳回 Q Promise 的程式庫,這沒問題,它會與新的 JavaScript Promise 順利運作。

不過,如我所說,jQuery 的 Deferreds 有點…沒什麼幫助。幸好您可以將這些項目轉換為標準 Promise,建議您盡快執行這項操作:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

在這裡,jQuery 的 $.ajax 會傳回 Deferred。由於它有 then() 方法,Promise.resolve() 可以將其轉換為 JavaScript Promise。不過,有時延遲會將多個引數傳遞至回呼,例如:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

但 JS Promise 會忽略第一個以外的所有項目:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

幸好這通常是您想要的結果,或至少能讓您存取想要的內容。另請注意,jQuery 不會遵循將 Error 物件傳遞至拒絕的慣例。

簡化複雜的非同步程式碼

好,我們來編寫一些程式碼。假設我們想要:

  1. 啟動微調器,表示正在載入
  2. 擷取故事的 JSON,其中包含標題和各章節的網址
  3. 為頁面新增標題
  4. 擷取每個章節
  5. 將故事新增至頁面
  6. 停止轉輪

…但如果過程中發生錯誤,也請告知使用者。我們也想在該時間點停止微調器,否則微調器會持續微調、暈眩,並撞上其他 UI。

當然,您不會使用 JavaScript 傳送故事,因為以 HTML 形式放送速度更快,但處理 API 時,這種模式相當常見:多次擷取資料,然後在所有資料擷取完畢後執行某些動作。

首先,我們來處理從網路擷取資料的問題:

將 XMLHttpRequest 轉換為 Promise

如果能以回溯相容的方式更新舊版 API,就會改用 Promise。XMLHttpRequest 是主要候選項目,但在此期間,我們先編寫簡單函式來發出 GET 要求:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

現在來使用這項功能:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

現在我們不必手動輸入 XMLHttpRequest 即可發出 HTTP 要求,這很棒,因為我越少看到 XMLHttpRequest 令人惱火的駝峰式大小寫,生活就越快樂。

鏈結

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 函式來使用 JSON responseType,但也可以在 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() 仍會傳回 Promise,但會擷取網址,然後將回應剖析為 JSON。

將非同步動作加入佇列

您也可以鏈結 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 發出非同步要求,取得一組要要求使用的網址,然後要求第一個網址。這時,Promise 真的開始從簡單的回呼模式中脫穎而出。

您甚至可以建立捷徑方法來取得章節:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

我們會等到呼叫 getChapter 時才下載 story.json,但下次呼叫 getChapter 時,我們會重複使用故事 Promise,因此只會擷取一次 story.json。Yay Promises!

處理錯誤

如先前所述,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 拒絕會略過,並以拒絕回呼 (或 catch(),因為兩者等效) 轉送至下一個 then()。使用 then(func1, func2) 時,系統會呼叫 func1func2,但不會同時呼叫兩者。但使用 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!");
})

上述流程與一般的 JavaScript try/catch 非常相似,在「try」中發生的錯誤會立即前往 catch() 區塊。以下是上述內容的流程圖 (因為我喜歡流程圖):

藍線代表已完成的 Promise,紅線則代表遭拒的 Promise。

JavaScript 例外狀況和 Promise

如果明確拒絕承諾,或在建構函式回呼中擲回錯誤,系統也會隱含拒絕承諾:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  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 的回呼,以及將 chapter1.html 新增至網頁的回呼。而是會移至 catch 回呼。因此,如果先前的任何動作失敗,頁面就會顯示「無法顯示章節」。

如同 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'

您可能只是想記錄 catch(),而不想從錯誤中復原。如要這麼做,只要重新擲回錯誤即可。我們可以在 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: for each url in story.chapterUrls, fetch & display
}).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';
})

但如何依序擷取章節網址並逐一處理?以下做法無效

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach 不支援非同步作業,因此章節會依下載順序顯示,這基本上就是《黑色追緝令》的編劇方式。這不是《黑色追緝令》,所以讓我們修正這個問題。

建立序列

我們想將 chapterUrls 陣列轉換為一連串的 Promise。我們可以使用 then() 執行這項操作:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

這是我們第一次看到 Promise.resolve(),這個函式會建立 Promise,並解析為您提供的任何值。如果您傳遞 Promise 的例項,系統只會傳回該例項 (注意:這是規格的變更,部分實作項目尚未遵循)。如果您傳遞類似 Promise 的內容 (具有 then() 方法),系統會建立真正的 Promise,並以相同方式完成/拒絕。如果您傳遞任何其他值,例如 Promise.resolve('Hello'),系統會建立以該值完成的 Promise。如未提供值就呼叫這個函式 (如上所示),系統會以「undefined」填入值。

此外,Promise.reject(val) 會建立承諾,並以您提供的值 (或未定義) 拒絕。

我們可以使用 array.reduce 整理上述程式碼:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the 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) {
    // 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';
})

這樣就完成了,這就是同步版本的完整非同步版本。但我們可以做得更好。目前網頁的下載方式如下:

瀏覽器很擅長一次下載多個項目,因此我們逐一下載章節會導致效能降低。我們希望同時下載所有檔案,然後在全部下載完成後再進行處理。 幸好有相關 API 可供使用:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all 會採用一系列 Promise,並在所有 Promise 成功完成時建立 Promise。您會取得一系列結果 (無論承諾完成的內容為何),順序與您傳入的承諾相同。

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) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

視連線情況而定,這項做法比逐一載入圖片快上幾秒,而且程式碼比第一次嘗試時更少。章節可以依任意順序下載,但會依正確順序顯示在畫面上。

不過,我們還是可以提升感知效能。第一章推出後,我們應將其新增至頁面。這樣一來,使用者就能在其他章節送達前開始閱讀。如果第三章推出,我們不會將其新增至頁面,因為使用者可能不會發現缺少第二章。第二章推出時,我們可以加入第二章和第三章,依此類推。

為此,我們會同時擷取所有章節的 JSON,然後建立序列,將這些章節新增至文件:

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 in 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';
})

這樣就大功告成,兩全其美!傳送所有內容所需的時間相同,但使用者會更快收到第一段內容。

在這個簡單的範例中,所有章節幾乎同時抵達,但如果章節數量更多、更大,一次顯示一個章節的好處就會更加明顯。

使用 Node.js 樣式的回呼或事件執行上述操作時,程式碼會增加一倍左右,但更重要的是,這類程式碼較難追蹤。不過,這並非 Promise 的最終樣貌,搭配其他 ES6 功能使用時,Promise 會更加簡單易用。

加分題:擴充功能

自我撰寫本文以來,使用 Promise 的功能已大幅擴展。自 Chrome 55 起,非同步函式允許以同步方式編寫以 Promise 為基礎的程式碼,但不會封鎖主執行緒。詳情請參閱這篇非同步函式文章。主要瀏覽器廣泛支援 Promise 和 async 函式。詳情請參閱 MDN 的「Promise」和「async 函式」參考資料。

感謝 Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 校對本文,並提供修正內容/建議。

此外,也感謝 Mathias Bynens更新文章的各個部分