谷歌博客:徹底弄懂JavaScript Promise

來自谷歌博客的一篇文章詳細的解釋了為什么Promise這么受歡迎、它用在哪些地方,具體的用法有哪些,怎么樣才能用好它。讀此文章,讓我徹底的深入了解了Promise的相關知識及其使用方法。以下為原文 女士們,先生們,請做好準備,迎接網頁開發史上的關鍵時刻。 [鼓點響起] Promise 已獲得 JavaScript 的原生支持! [煙花綻放、彩紙飄飄、人群沸騰] 此刻,您可能屬于以下其中某一類:

  • 人群在您身邊歡呼雀躍,但是您感到莫名其妙??赡苣踔吝B“promise”是什么都不知道。因此您聳聳肩,但是從天而降的彩紙雖輕如鴻毛卻讓您無法釋懷。如果真是這樣,您也無需擔心,我可花了很長的時間才弄明白為什么我應該關注它。您可能想從開始處開始。
  • 您非常抓狂!覺得晚了一步,對嗎?您可能之前使用過這些 Promise,但讓您困擾的是,不同版本的 API 各有差異。JavaScript 官方版本的 API 是什么?您可能想要從術語開始。
  • 您已知道這些,您會覺得那些上竄下跳的人很好笑,居然把它當作新聞。您可以先自豪一把,然后直接查看 API 參考

人們究竟為何歡呼雀躍?

JavaScript 是單線程工作,這意味著兩段腳本不能同時運行,而是必須一個接一個地運行。在瀏覽器中,JavaScript 與因瀏覽器而異的其他 N 種任務共享一個線程。但是通常情況下 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 已成功或失敗,且您之后添加了成功/失敗回調,則將會調用正確的回調,即使事件發生在先。
這對于異步成功/失敗尤為有用,因為您可能對某些功能可用的準確時間不是那么關注,更多地是關注對結果作出的反應。

Promise 術語

Domenic Denicola 校對了本篇文章的初稿,并在術語方面給我打分為“F”。他把我留下來,強迫我抄寫狀態和結果 100 遍,并給我的父母寫了封告狀信。盡管如此,我還是對很多術語混淆不清,以下是幾個基本的概念: promise 可以是:
  • 已執行 - 與 promise 有關的操作成功
  • 已拒絕 - 與 promise 有關的操作失敗
  • 待定 - 尚未執行或拒絕
  • 已解決 - 已執行或拒絕
本規范還使用術語 thenable 來描述類似于 promise 的對象,并使用 then 方法。該術語讓我想起前英格蘭國家隊教練 Terry Venables,因此我將盡可能不用這個術語。

Promise 在 JavaScript 中受支持!

Promise 有一段時間以庫的形式出現,例如: 以上這些與 JavaScript promise 都有一個名為 Promise/A+ 的常見標準化行為。如果您是 jQuery 用戶,他們還有一個類似于名為 Deferred 的行為。但是,Deferred 與 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 對象,但這不是必須的。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。在 JavaScript 中使用比在 DOM 中更好,因為它們將在如 Node.js 等非瀏覽器 JS 環境中可用(而它們是否會在核心 API 中使用 Promise 則是另外一個問題)。 盡管它們是 JavaScript 的一項功能,但 DOM 也能使用。實際上,采用異步成功/失敗方法的所有新 DOM API 均使用 promise。Quota Management、Font Load Events、ServiceWorker、Web MIDIStreams 等等都已經在使用 promise。

瀏覽器支持和 polyfill

現在,promise 已在各瀏覽器中實現。 在 Chrome 32、Opera 19、Firefox 29、Safari 8 和 Microsoft Edge 中,promise 默認啟用。 如要使沒有完全實現 promise 的瀏覽器符合規范,或向其他瀏覽器和 Node.js 中添加 promise,請查看 polyfill(gzip 壓縮大小為 2k)。

與其他庫的兼容性

JavaScript promise API 將任何使用 then() 方法的結構都當作 promise 一樣(或按 promise 的說法為 thenable)來處理,因此,如果您使用返回 Q promise 的庫也沒問題,因為它能與新 JavaScript promise 很好地兼容。 如我之前所提到的,jQuery 的 Deferred 不那么有用。幸運的是,您可以將其轉為標準 promise,這值得盡快去做:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
這里,jQuery 的 $.ajax 返回了一個 Deferred。由于它使用 then() 方法,因此 Promise.resolve() 可將其轉為 JavaScript promise。但是,有時 deferred 會將多個參數傳遞給其回調,例如:
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 對象傳遞到 reject 這一慣例。

復雜異步代碼讓一切變得更簡單

對了,讓我們寫一些代碼。比如說,我們想要:
  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);
})
點擊此處了解實際操作,檢查 DevTools 中的控制臺以查看結果?,F在我們無需手動鍵入 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);
})
這里的 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);
})
了解實際操作,檢查 DevTools 中的控制臺以查看結果。實際上,我們可以讓 getJSON() 函數更簡單:
function getJSON(url) {
  return get(url).then(JSON.parse);
}
getJSON() 仍返回一個 promise,該 promise 獲取 URL 后將 response 解析為 JSON。

異步操作隊列

您還可以鏈接多個 then,以便按順序運行異步操作。 當您從 then() 回調中返回某些內容時,這有點兒神奇。如果返回一個值,則會以該值調用下一個 then()。但是,如果您返回類似于 promise 的內容,下一個 then() 則會等待,并僅在 promise 產生結果(成功/失?。r調用。例如:
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 romise,因此 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),則 func1func2 中的一個將被調用,而不會二者均被調用。但如果是 then(func1).catch(func2),則在 func1 拒絕時兩者均被調用,因為它們在該鏈中是單獨的步驟??纯聪旅娴拇a:
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

當 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 回調。因此,如果任一前述操作失敗,“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';
})
但是我們如何遍歷章節的 URL 并按順序獲取呢?以下方法行不通
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),它創建的 promise 在拒絕時將返回賦予的值(或“undefined”)。 我們可以使用 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 執行的結果),其順序與您與傳入 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 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-style 回調或事件來執行以上示例需兩倍代碼,更重要的是,沒那么容易實施。然而,promise 功能還不止如此,與其他 ES6 功能組合使用時,它們甚至更容易。

友情贈送:promise 和 generator

以下內容涉及一整套 ES6 新增功能,但您目前在使用 promise 編碼時無需掌握它們??蓪⑵湟暈榧磳⑸嫌车暮萌R塢大片電影預告。 ES6 還為我們提供了 generator,它可讓某些功能在某個位置退出(類似于“return”),但之后能以相同位置和狀態恢復,例如:
function *addGenerator() {
  var i = 0;
  while (true) {
    i += yield i;
  }
}
注意函數名稱前面的星號,這表示 generator。yield 關鍵字是我們的返回/恢復位置。我們可按下述方式使用:
var adder = addGenerator();
adder.next().value; // 0
adder.next(5).value; // 5
adder.next(5).value; // 10
adder.next(5).value; // 15
adder.next(50).value; // 65
但是這對于 promise 而言意味著什么呢?您可以使用返回/恢復行為來編寫異步代碼,這些代碼看起來像同步代碼,而且實施起來也與同步代碼一樣簡單。對各行代碼的理解無需過多擔心,借助于幫助程序函數,我們可使用 yield 來等待 promise 得到解決:
function spawn(generatorFunc) {
  function continuer(verb, arg) {
    var result;
    try {
      result = generator[verb](arg);
    } catch (err) {
      return Promise.reject(err);
    }
    if (result.done) {
      return result.value;
    } else {
      return Promise.resolve(result.value).then(onFulfilled, onRejected);
    }
  }
  var generator = generatorFunc();
  var onFulfilled = continuer.bind(continuer, "next");
  var onRejected = continuer.bind(continuer, "throw");
  return onFulfilled();
}
…在上述示例中我幾乎是從 Q 中逐字般過來,并針對 JavaScript promise 進行了改寫。因此,我們可以采用顯示章節的最后一個最佳示例,結合新 ES6 的優勢,將其轉變為:
spawn(function *() {
  try {
    // 'yield' effectively does an async wait,
    // returning the result of the promise
    let story = yield getJSON('story.json');
    addHtmlToPage(story.heading);

    // Map our array of chapter urls to
    // an array of chapter json promises.
    // This makes sure they all download parallel.
    let chapterPromises = story.chapterUrls.map(getJSON);

    for (let chapterPromise of chapterPromises) {
      // Wait for each chapter to be ready, then add it to the page
      let chapter = yield chapterPromise;
      addHtmlToPage(chapter.html);
    }

    addTextToPage("All done");
  }
  catch (err) {
    // try/catch just works, rejected promises are thrown here
    addTextToPage("Argh, broken: " + err.message);
  }
  document.querySelector('.spinner').style.display = 'none';
})
試一下 這跟之前的效用完全相同,但讀起來容易多了。Chrome 和 Opera 當前支持該功能(查看代碼),而且 Microsoft Edge 中也可使用該功能(需要在 about:flags 中打開 Enable experimental JavaScript features 設置)。在即將發布的版本中,該功能默認啟用。 它將納入很多新的 ES6 元素:promise、generator、let、for-of。我們生成一個 promise 后,spawn 幫助程序將等待該 promise 來解析并返回一個終值。如果 promise 拒絕,spawn 會讓 yield 語句拋出異常,我們可通過常規的 JavaScript try/catch 來捕獲此異常。異步編碼竟如此簡單! 此模式非常有用,在 ES7 中它將以異步功能的形式提供。它幾乎與上述編碼示例相同,但無需使用 spawn 方法。

Promise API 參考

所有方法在 Chrome、Opera、Firefox、Microsoft Edge 和 Safari 中均可使用,除非另有說明。polyfill 為所有瀏覽器提供以下方法。

靜態方法

方法匯總
Promise.resolve(promise); 返回 promise(僅當 promise.constructor == Promise 時)
Promise.resolve(thenable); 從 thenable 中生成一個新 promise。thenable 是具有 `then()` 方法的類似于 promise 的對象。
Promise.resolve(obj); 在此情況下,生成一個 promise 并在執行時返回 obj。
Promise.reject(obj); 生成一個 promise 并在拒絕時返回 obj。為保持一致和調試之目的(例如堆疊追蹤), obj 應為 instanceof Error。
Promise.all(array); 生成一個 promise,該 promise 在數組中各項執行時執行,在任意一項拒絕時拒絕。每個數組項均傳遞給 Promise.resolve,因此數組可能混合了類似于 promise 的對象和其他對象。執行值是一組有序的執行值。拒絕值是第一個拒絕值。
Promise.race(array); 生成一個 Promise,該 Promise 在任意項執行時執行,或在任意項拒絕時拒絕,以最先發生的為準。
注:我對 Promise.race 的實用性表示懷疑;我更傾向于使用與之相對的 Promise.all,它僅在所有項拒絕時才拒絕。

構造函數

構造函數
new Promise(function(resolve, reject) {}); resolve(thenable) Promise 依據 thenable 的結果而執行/拒絕。 resolve(obj) Promise 執行并返回 obj reject(obj) Promise 拒絕并返回 obj。為保持一致和調試(例如堆疊追蹤),obj 應為 instanceof Error。 在構造函數回調中引發的任何錯誤將隱式傳遞給 reject()。

實例方法

的錦上添花
實例方法
promise.then(onFulfilled, onRejected) 當/如果“promise”解析,則調用 onFulfilled。當/如果“promise”拒絕,則調用 onRejected。 兩者均可選,如果任意一個或兩者都被忽略,則調用鏈中的下一個 onFulfilled/onRejected。兩個回調都只有一個參數:執行值或拒絕原因。 then() 將返回一個新 promise,它相當于從 onFulfilled/onRejected 中返回、 通過 Promise.resolve 傳遞的值。如果在回調中引發了錯誤,返回的 promise 將拒絕并返回該錯誤。
promise.catch(onRejected) promise.then(undefined, onRejected)
Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 對本篇文章進行了校對,提出了建議并作出了修正,特此感謝! 此外,Mathias Bynens 負責本篇文章的更新部分,特此致謝。