JavaScript Promise:簡介

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

Jake Archibald
Jake Archibald

網頁程式開發人員,請做好準備,迎接網頁開發歷史上的關鍵時刻。

[Drumroll begins]

JavaScript 推出 Promise 了!

[煙火綻放,亮片紙從天而降,群眾歡呼]

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

  • 周遭的人都在歡呼,但你不知道大家在興奮什麼。你甚至可能不確定什麼是「承諾」。你聳了聳肩,但閃閃發光的紙張重量壓在你的肩膀上。如果是的話,別擔心,我花了很長時間才弄清楚為什麼我應該關心這些東西。建議您從開頭開始。
  • 你揮拳擊向空中!總算修好了!您之前用過這些 Promise,但所有實作的 API 略有不同,讓您感到困擾。官方 JavaScript 版本的 API 是什麼?建議您先瞭解術語。
  • 你早就知道這件事,並嘲笑那些大驚小怪的人。請花點時間享受自己的優越感,然後直接前往 API 參考資料。

瀏覽器支援和 Polyfill

Browser Support

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

Source

如要讓缺乏完整 Promise 實作的瀏覽器符合規格,或將 Promise 新增至其他瀏覽器和 Node.js,請參閱 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 並未提供這樣的方法。此外,這會載入一張圖片。如果想知道一組圖片何時載入,情況會變得更複雜。

事件不一定是最佳做法

事件適合用於同一個物件上可能發生多次的情況,例如 keyup、touchstart 等。在這些事件中,您並不在意附加事件監聽器之前發生的情況。但如果是非同步成功/失敗,理想情況下,您會希望看到類似這樣的結果:

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 術語

Domenic Denicola 校對了本文的第一份草稿,並在術語方面給了我「F」評分。他把我留校察看,強迫我抄寫《States and Fates》100 次,還寫了一封信給我的父母,表達他的擔憂。儘管如此,我還是經常搞混許多術語,但以下是基本概念:

承諾可以是:

  • fulfilled:與 Promise 相關的動作已成功
  • rejected:與 Promise 相關的動作失敗
  • pending:尚未履行或拒絕
  • settled:已履行或拒絕

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

JavaScript 推出 Promise!

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

上述和 JavaScript 承諾共用一種稱為 Promises/A+ 的標準化行為。如果您是 jQuery 使用者,他們有類似的行為,稱為 Deferreds。不過,Deferreds 不符合 Promise/A+ 規範,因此略有不同且較不實用,請務必留意。jQuery 也有 Promise 類型,但這只是 Deferred 的子集,且有相同的問題。

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

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 物件拒絕,但並非必要。Error 物件的好處是會擷取堆疊追蹤,讓偵錯工具更實用。

使用方式如下:

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

then() 採用兩個引數,一個用於成功案例的回呼,另一個用於失敗案例。兩者皆為選用,因此您可以只為成功或失敗情況新增回呼。

JavaScript Promise 一開始在 DOM 中稱為「Future」,後來改名為「Promise」,最後才移至 JavaScript。將這些 API 放在 JavaScript 而非 DOM 中,好處是它們可在非瀏覽器 JS 環境 (例如 Node.js) 中使用 (至於是否在核心 API 中使用,則是另一個問題)。

雖然它們是 JavaScript 功能,但 DOM 仍會使用。事實上,所有具有非同步成功/失敗方法的新 DOM API 都會使用 Promise。配額管理、字型載入事件、ServiceWorker、Web MIDI、串流等功能已採用這項做法。

與其他程式庫的相容性

JavaScript Promise API 會將任何具有 then() 方法的項目視為類似 Promise (或 Promise 語言中的 thenable,嘆氣),因此如果您使用的程式庫會傳回 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 時,這種模式相當常見:多次擷取資料,然後在所有作業完成後執行某些操作。

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

Promisifying XMLHttpRequest

如果能以回溯相容的方式更新舊版 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 時,我們會重複使用故事承諾,因此只會擷取 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 拒絕會跳到下一個具有拒絕回呼的 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!");
})

上述流程與一般的 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')),則會建立一個承諾,並以該值履行。如上所示,如果呼叫時未提供值,則會以「undefined」執行。

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

我們可以使用 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 和非同步函式。詳情請參閱 MDN 的 Promise 和非同步函式參考資料。

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

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