JavaScript Promise:簡介

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

Jake Archibald
Jake Archibald

開發人員,請為網頁開發史上的關鍵時刻做好準備。

[Drumroll begins]

JavaScript 終於支援 Promise 了!

[煙火爆開,閃亮的紙片從天而降,觀眾歡呼]

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

  • 周圍的人群歡呼,但您不確定發生了什麼事。或許你甚至不確定「承諾」是什麼。你會聳聳肩,但亮片紙的重量會壓在你的肩膀上。如果是這樣,請放心,我花了好幾年才想通為何要關心這些事情。建議您從一開始就開始。
  • 你揮拳擊空氣!差不多了,對吧?您之前曾使用這些 Promise 項目,但所有實作項目的 API 都略有不同,這讓您感到困擾。官方 JavaScript 版本的 API 為何?您可能想先從術語開始。
  • 你已經知道這件事,並且嘲笑那些對此感到興奮不已的人,好像這對他們來說是新聞一樣。請花點時間享受自己的優越感,然後直接前往 API 參考資料

瀏覽器支援和 polyfill

瀏覽器支援

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

資料來源

如要讓缺乏完整承諾實作的瀏覽器符合規格,或在其他瀏覽器和 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 沒有提供這種方法。此外,這會載入一張圖片。如果想知道一組圖片何時載入,情況就會更加複雜。

事件不一定是最理想的做法

事件非常適合在同一個物件上多次發生的事件,例如 keyuptouchstart 等。對於這些事件,您不必在意附加事件監聽器之前發生的事件。但就非同步成功/失敗而言,理想情況下,您應該會希望看到類似以下的情況:

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

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

這就是承諾的功能,但名稱更易懂。如果 HTML 圖片元素具有傳回承諾的「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 只能成功或失敗一次。它無法成功或失敗兩次,也無法從成功切換為失敗,反之亦然。
  • 如果承諾已成功或失敗,且您稍後新增成功/失敗回呼,系統會呼叫正確的回呼,即使事件已在先前發生也一樣。

這對於非同步成功/失敗非常實用,因為您較不關心某項內容可供使用的精確時間,而更關心對結果做出反應。

承諾術語

Domenic Denicola 為本文第一個草稿進行校對,並給予我「F」的成績,原因是用詞不當。他將我留在學校,強迫我抄寫「States and Fates」100 次,並寫信給我的父母,表達他的擔憂。儘管如此,我還是會將許多術語搞混,以下列出基本概念:

承諾可以是:

  • fulfilled:與承諾相關的動作成功
  • rejected - 與承諾相關的動作失敗
  • pending - 尚未完成或拒絕
  • settled - 已完成或拒絕

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

JavaScript 支援 Promise!

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

上述和 JavaScript 承諾共用一個稱為 Promises/A+ 的共同標準行為。如果您是 jQuery 使用者,則有類似的 Deferreds。不過,延遲函式不符合 Promise/A+ 規範,因此與 Promise 略有不同,實用性也較低,請留意這點。jQuery 也有Promise 類型,但這只是延遲函式的子集,且有相同的問題。

雖然承諾實作項目遵循標準化行為,但整體 API 會有所不同。JavaScript 承諾在 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"));
 
}
});

承諾建構函式會採用一個引數,也就是具有兩個參數的回呼: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 承諾最初在 DOM 中以「Futures」的形式出現,後來改名為「Promises」,最後移至 JavaScript。將這些屬性放在 JavaScript 中而非 DOM 中,會是個不錯的做法,因為這樣就能在 Node.js 等非瀏覽器 JS 情境中使用這些屬性 (是否在核心 API 中使用這些屬性則是另一個問題)。

雖然這些是 JavaScript 功能,但 DOM 不怕使用這些功能。事實上,所有使用非同步成功/失敗方法的新 DOM API 都會使用承諾。這項做法已在配額管理字型載入事件ServiceWorkerWeb MIDI串流等功能中實現。

與其他程式庫的相容性

JavaScript 承諾 API 會將任何含有 then() 方法的項目視為類似承諾的項目 (或承諾語言中的 thenable sigh),因此如果您使用會傳回 Q 承諾的程式庫,那麼該程式庫會與新的 JavaScript 承諾搭配使用。

不過,如先前所述,jQuery 的 Deferred 有點...沒什麼幫助。幸好,您可以將這些項目轉換為標準承諾,建議您盡快這麼做:

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

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

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

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

而 JS 承諾會忽略第一個以外的所有承諾:

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

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

簡化複雜的非同步程式碼

好,我們來寫程式吧。假設我們想:

  1. 啟動旋轉圖示,表示正在載入
  2. 擷取故事的部分 JSON,以便取得標題和各章節的網址
  3. 為頁面新增標題
  4. 擷取每個章節
  5. 將短片故事加入頁面
  6. 停止旋轉圖示

但如果過程中發生錯誤,也要通知使用者。我們也會在這個時候停止旋轉圖示,否則它會持續旋轉、變得混亂,並與其他 UI 發生衝突。

當然,您不會使用 JavaScript 提交故事,以 HTML 呈現會更快,但在處理 API 時,這種模式相當常見:多次擷取資料,然後在完成時執行某項操作。

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

將 XMLHttpRequest 轉換為承諾

如果可以以回溯相容的方式,舊版 API 將會更新為使用承諾。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);
})

我們現在可以發出 HTTP 要求,而無需手動輸入 XMLHttpRequest,這真是太棒了,因為我越少看到令人惱火的駝峰式 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,但我們也可以在承諾中解決這個問題:

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,該 Promise 會擷取網址,然後將回應解析為 JSON。

將非同步動作排入佇列

您也可以鏈結 then,依序執行非同步動作。

then() 回呼傳回內容時,會發生一些神奇的情況。如果您傳回值,系統會使用該值呼叫下一個 then()。不過,如果您傳回類似承諾的內容,下一個 then() 會等待該內容,並且只在該承諾完成 (成功/失敗) 時才會呼叫。例如:

getJSON('story.json').then(function(story) {
 
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console
.log("Got chapter 1!", chapter1);
})

我們在這裡向 story.json 提出非同步要求,這會提供一組可要求的網址,然後我們會要求其中的第一個網址。這時承諾才會開始脫穎而出,與簡單的回呼模式截然不同。

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

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 一次。耶!Promises!

處理錯誤

如先前所述,then() 會採用兩個引數,一個用於成功,另一個用於失敗 (或以承諾來說,一個用於完成,另一個用於拒絕):

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

雖然差異不大,但非常實用。承諾拒絕會跳過下一個 then(),並使用拒絕回呼 (或 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() 區塊。以下是上述內容的流程圖 (因為我喜歡流程圖):

請按照藍色線條執行已兌現的承諾,或按照紅色線條執行已拒絕的承諾。

JavaScript 例外狀況和承諾

當承諾明確遭到拒絕時,系統會拒絕承諾,但如果建構函式回呼中擲回錯誤,也會隱含拒絕承諾:

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

也就是說,在承諾建構函式回呼中執行所有承諾相關工作很有用,這樣系統就能自動偵測錯誤並拒絕執行。

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 回呼。因此,如果先前的任何動作失敗,頁面就會加入「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'

您可能只想 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 陣列轉換為承諾序列。我們可以使用 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 的例項,它就會直接傳回 (注意:這是對規格進行的變更,但某些實作尚未遵循此規格)。如果您傳遞類似承諾的內容 (具有 then() 方法),系統會建立真正的 Promise,以相同方式完成/拒絕。如果您傳入任何其他值 (例如 Promise.resolve('Hello'),它會建立一個與該值相符的承諾。如果您呼叫時未提供任何值,則會如上所述,以「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 會取用承諾陣列,並建立在所有承諾成功完成時執行的承諾。您會以與傳入的承諾相同的順序,取得一組結果 (無論承諾是否已兌現)。

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 樣式的回呼或事件執行上述操作,所需程式碼約為上述程式碼的兩倍,但更重要的是,這麼做不容易理解。不過,這並不是承諾的結束,如果與其他 ES6 功能搭配使用,承諾的使用方式會更簡單。

加分賽:擴充功能

自從我最初撰寫這篇文章以來,使用 Promise 的功能已大幅擴增。自 Chrome 55 起,非同步函式允許以同步方式編寫以承諾為基礎的程式碼,但不會封鎖主執行緒。如要進一步瞭解這項功能,請參閱我的非同步函式文章。主要瀏覽器廣泛支援 Promise 和非同步函式。詳情請參閱 MDN 的 Promise非同步函式參考資料。

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

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