[譯] 一個簡單的 ES6 Promise 指南

The woods are lovely, dark and deep. But I have promises to keep, and miles to go before I sleep. — Robert Frostjavascript

Promise 是 JavaScript ES6 中最使人興奮的新增功能之一。爲了支持異步編程,JavaScript 使用了回調(callbacks),以及一些其餘的技術。然而,使用回調會遇到地獄回調/末日金字塔等問題。Promise 是一種經過使代碼看起來同步並避免在回調時出現問題進而大大簡化異步編程的模式。html

在這篇文章中,咱們將看到什麼是 Promise,以及如何利用它給咱們帶來好處。前端

什麼是 Promise?

ECMA 委員會將 promise 定義爲 ——java

Promise 是一個對象,是一個用做延遲(也多是異步)計算的最終結果的佔位符。node

簡單來講,一個 promise 是一個裝有將來值的容器。若是你仔細想一想,這正是你正常的平常談話中使用承諾(promise)這個詞的方式。好比,你預約一張去印度的機票,準備前往美麗的山崗站大吉嶺旅遊。預訂後,你會獲得一張機票。這張機票是航空公司的一個承諾,意味着你在出發當天能夠得到相應的座位。實質上,票證是將來值的佔位符,即座位android

這還有另一個例子 —— 你向你的朋友承諾,你會在看完計算機程序設計藝術這本書後還給他們。在這裏,你的話充當佔位符。值就至關於這本書。ios

你能夠想一想其餘相似承諾(promise)的例子,這些例子涉及各類現實生活中的狀況,例如在醫生辦公室等候,在餐廳點餐,在圖書館發放書籍等等。這些全部的狀況都涉及某種形式的承諾(promise)。然而,例子只能告訴咱們這麼多,Talk is cheap, so let’s see the code.git

建立 Promise

當某個任務的完成時間不肯定或太長時,咱們能夠建立一個 promise 。例如 —— 根據鏈接速度的不一樣,一個網絡請求可能須要 10 ms 甚至須要 200 ms 這麼久。咱們不想等待這個數據獲取的過程。對你而言,200 ms 可能看起來不多,但對於計算機來講是一段很是漫長的時間。promise 的目的就是讓這種異步(asynchrony)變得簡單而輕鬆。讓咱們一塊兒來看看基礎知識。es6

使用 Promise 構造函數建立了一個新的 promise。像這樣 ——github

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 <= 90) {
        resolve('Hello, Promises!');
    }
    reject(new Error('In 10% of the cases, I fail. Miserably.'));
});
複製代碼

Promise 示例

觀察這個構造函數就能夠發現其接收一個帶有兩個參數的函數,這個函數被稱爲執行器函數,而且它描述了須要完成的計算。執行器函數的參數一般被稱爲 resolvereject,分別標記執行器函數的成功和不成功的最終完成結果。

resolvereject 自己也是函數,它們用於將返回值返回給 promise 對象。當計算成功或將來值準備好時,咱們使用 resolve 函數將值返回。這時咱們說這個 promise 已經被成功解決(resolve)了

若是計算失敗或遇到錯誤,咱們經過在 reject 函數中傳遞錯誤對象告知 promise 對象。 這時咱們說這個 promise 已經被拒絕(reject)了reject 能夠接收任何類型的值。可是,建議傳遞一個 Error 對象,由於它能夠經過查看堆棧跟蹤來幫助調試。

在上面的例子中,Math.random() 用於生成一個隨機數。有 90% 機率,這個 promise 會被成功解決(假設機率均勻分佈)。其他的狀況則會被拒絕。

使用 Promise

在上面的例子中,咱們建立了一個 promise 並將其存儲在 myPromise 中。那咱們如何才能獲取經過 resolve reject 函數傳遞過來的值呢?全部的 Promise 都有一個 .then() 方法。這樣問題就好解決了,讓咱們一塊兒來看一下 ——

const myPromise = new Promise((resolve, reject) => {
    if (Math.random() * 100 < 90) {
        console.log('resolving the promise ...');
        resolve('Hello, Promises!');
    }
    reject(new Error('In 10% of the cases, I fail. Miserably.'));
});

// 兩個函數
const onResolved = (resolvedValue) => console.log(resolvedValue);
const onRejected = (error) => console.log(error);

myPromise.then(onResolved, onRejected);

// 效果同上,代碼更加簡明扼要
myPromise.then((resolvedValue) => {
    console.log(resolvedValue);
}, (error) => {
    console.log(error);
});

// 有 90% 的機率輸出下面語句

// resolving the promise ...
// Hello, Promises!
// Hello, Promises!
複製代碼

使用 Promise

.then() 接收兩個回調函數。第一個回調在 promise 被解決時調用。第二個回調在 promise 被拒絕時調用。

兩個函數分別在第 10 行和第 11 行定義,即 onResolvedonRejected。它們做爲回調傳遞給第 13 行中的 .then()。你也可使用第 16 行到第 20 行更常見的 .then 寫做風格。它提供了與上述寫法相同的功能。

在上面的例子中還有一些須要注意的重要事項。

咱們建立了一個 promise 實例 myPromise。咱們分別在第 13 行和第 16 行附加了兩個 .then 的處理程序。儘管它們在功能上是相同的,但它們仍是被被視爲不一樣的處理程序。可是 ——

  • 一個 promise 只能成功(resolved)或失敗(reject)一次。它不能成功或失敗兩次,也不能從成功切換到失敗,反之亦然。
  • 若是一個 promise 在你添加成功/失敗回調(即 .then)以前就已經成功或者失敗,則 promise 仍是會正確地調用回調函數,即便事件發生地比添加回調函數要早。

這意味着一旦 promise 達到最終狀態,即便你屢次附加 .then 處理程序,狀態也不會改變(即不會再從新開始計算)。

爲了驗證這一點,你能夠在第3行看到一個 console.log 語句。當你用 .then 處理程序運行上述代碼時,須要輸出的語句只會被打印一次。它代表 promise 緩存告終果,而且下次也會獲得相同的結果

另外一個要注意的是,promise 的特色是及早求值(evaluated eagerly)只要聲明並將其綁定到變量,就當即開始執行。沒有 .start.begin 方法。就像在上面的例子中那樣。

爲了確保 promise 不是當即開始而是惰性求值(evaluates lazily),咱們將它們包裝在函數中。稍後會看到一個例子。

捕捉 Promise

到目前爲止,咱們只是很方便地看到了 resolve 的案例。那當執行器函數發生錯誤的時候會發生什麼呢?當發生錯誤時,執行 .then() 的第二個回調,即 onRejected。讓咱們來看一個例子 ——

const myProimse = new Promise((resolve, reject) => {
  if (Math.random() * 100 < 90) {
    reject(new Error('The promise was rejected by using reject function.'));
  }
  throw new Error('The promise was rejected by throwing an error');
});

myProimse.then(
  () => console.log('resolved'), 
  (error) => console.log(error.message)
);

// 有 90% 的機率輸出下面語句

// The promise was rejected by using reject function.
複製代碼

Promise 出錯

這與第一個例子相同,但如今它以 90% 的機率執行 reject 函數,而且剩下的 10% 的狀況會拋出錯誤。

在第 10 和 11 行,咱們分別定義了 onResolvedonRejected 回調。請注意,即便發生錯誤,onRejected 也會執行。所以咱們沒有必要經過在 reject 函數中傳遞錯誤來拒絕一個 promise。也就是說,這兩種狀況下的 promise 都會被拒絕。

因爲錯誤處理是健壯程序的必要條件,所以 promise 爲這種狀況提供了一條捷徑。當咱們想要處理一個錯誤時,咱們可使用 .catch(onRejected) 接收一個回調:onRejected,而沒必要使用 .then(null, () => {...})。如下代碼將展現如何使用 catch 處理程序 ——

myProimse.catch(  
  (error) => console.log(error.message)  
);
複製代碼

請記住 .catch 只是 .then(undefined, onRejected) 的一個語法糖

Promise 鏈式調用

.then().catch() 方法老是返回一個 promise。因此你能夠把多個 .then 連接到一塊兒。讓咱們經過一個例子來理解它。

首先,咱們建立一個返回 promise 的 delay 函數。返回的 promise 將在給定秒數後解析。這是它的實現 ——

const delay = (ms) => new Promise(  
  (resolve) => setTimeout(resolve, ms)  
);
複製代碼

在這個例子中,咱們使用一個函數來包裝咱們的 promise,以便它不會當即執行。該 delay 函數接收以毫秒爲單位的時間做爲參數。因爲閉包的特色,該執行器函數能夠訪問 ms 參數。它還包含一個在 ms 毫秒後調用 resolve 函數的 setTimeout 函數,從而有效解決 promise。這是一個示例用法 ——

delay(5000).then(() => console.log('Resolved after 5 seconds'));
複製代碼

只有在 delay(5000) 解決後,.then 回調中的語句纔會運行。當你運行上面的代碼時,你會在 5 秒後看到 Resolved after 5 seconds 被打印出來。

如下是咱們如何實現 .then() 的鏈式調用 ——

const delay = (ms) => new Promise(
  (resolve) => setTimeout(resolve, ms)
);

delay(2000)
  .then(() => {
    console.log('Resolved after 2 seconds')
    return delay(1500);
  })
  .then(() => {
    console.log('Resolved after 1.5 seconds');
    return delay(3000);
  }).then(() => {
    console.log('Resolved after 3 seconds');
    throw new Error();
  }).catch(() => {
    console.log('Caught an error.');
  }).then(() => {
    console.log('Done.');
  });

// Resolved after 2 seconds
// Resolved after 1.5 seconds
// Resolved after 3 seconds
// Caught an error.
// Done.
複製代碼

Promise 鏈式調用

咱們從第 5 行開始。所採起的步驟以下 ——

  • delay(2000) 函數返回一個在兩秒以後能夠獲得解決的 promise。
  • 第一個 .then() 執行。它輸出了一個句子 Resolved after 2 seconds。而後,它經過調用 delay(1500) 返回另外一個 promise。若是一個 .then() 裏面返回了一個 promise,該 promise 的**解決方案(技術上稱爲結算)**是轉發給下一個 .then 去調用。
  • 鏈式調用持續到最後。

另請注意第 15 行。咱們在 .then 裏面拋出了一個錯誤。那意味着當前的 promise 被拒絕了,並被下一個 .catch 處理程序捕捉。所以,Caught an error 這句話被打印。然而,一個 .catch 自己老是被解析爲 promise,而且不會被拒絕(除非你故意拋出錯誤)。這就是爲何 .then 後面的 .catch 會被執行的緣由。

這裏建議使用 .catch 而不是帶有 onResolvedonRejected 參數的 .then 去處理。下面有一個案例解釋了爲何最好這樣作 ——

const promiseThatResolves = () => new Promise((resolve, reject) => {
  resolve();
});

// 致使被拒絕的 promise 沒有被處理
promiseThatResolves().then(
  () => { throw new Error },
  (err) => console.log(err),
);

// 適當的錯誤處理
promiseThatResolves()
  .then(() => {
    throw new Error();
  })
  .catch(err => console.log(err));
複製代碼

第 1 行建立了一個始終能夠解決的 promise。當你有一個帶有兩個回調 ,即 onResolvedonRejected.then 方法時,你只能處理執行器函數的錯誤和拒絕。假設 .then 中的處理程序也會拋出錯誤。它不會致使執行 onRejected 回調,如第 6 - 9 行所示。

但若是你在 .then 後跟着調用 .catch,那麼 .catch 既捕捉執行器函數的錯誤也捕捉 .then 處理程序的錯誤。這是有道理的,由於 .then 老是返回一個 promise。如第 12 - 16 行所示。


你能夠執行全部的代碼示例,並經過實踐應用學的更多。一個好的學習方法是將 promise 經過基於回調的函數從新實現。若是你使用 Node,那麼在 fs 和其餘模塊中的不少函數都是基於回調的。在 Node 中確實存在能夠自動將基於回調的函數轉換爲 promise 的實用工具,例如 util.promisifypify。可是,若是你還在學習階段,請考慮遵循 WET(Write Everything Twice)原則,並從新實現或閱讀儘量多的庫/函數的代碼。若是不是在學習階段,特別是在生產環境下,請每隔一段時間就要使用 DRY(Don’t Repeat Yourself) 原則激勵本身。

還有不少其餘的 promise 相關知識我沒有說起,好比 Promise.allPromise.race 和其餘靜態方法,以及如何處理 promise 中出現的錯誤,還有一些在建立一個promise 時應該注意的一些常見的反模式(anti-patterns)和細節。你能夠參考下面的文章,以即可以更好地瞭解這些主題。

若是你但願我在另外一篇文章中涵蓋這些主題,請回複本文!:)


參考

我但願你能喜歡這個客串貼!本文由 Arfat Salmon 專門爲 CodeBurst.io 撰寫

結束語

感謝閱讀!若是你最終決定走上 web 開發這條不歸路,請查看:2018 年 Web 開發人員路線圖

若是你正在努力成爲一個更好的 JavaScript 開發人員,請查看:提升你的 JavaScript 面試水平 ——  學習算法 + 數據結構

若是你但願成爲我每週一次的電子郵件列表中的一員,請考慮在此輸入你的 email,或者在 Twitter 上關注我。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索