初學者應該看的JavaScript Promise 完整指南

2021-03-02 大遷世界

這篇文章算是 JavaScript Promises 比較全面的教程,該文介紹了必要的方法,例如 then,catch和finally。此外,還包括處理更複雜的情況,例如與Promise.all並行執行Promise,通過Promise.race 來處理請求超時的情況,Promise 鏈以及一些最佳實踐和常見的陷阱。

1.JavaScript Promises

Promise 是一個允許我們處理異步操作的對象,它是 es5 早期回調的替代方法。

與回調相比,Promise 具有許多優點,例如:

提供組合錯誤處理。* 更好的流程控制,可以讓異步並行或串行執行。

回調更容易形成深度嵌套的結構(也稱為回調地獄)。如下所示:

a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

如果將這些函數轉換為 Promise,則可以將它們連結起來以生成更可維護的代碼。像這樣:

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error);

在上面的示例中,Promise 對象公開了.then和.catch方法,我們稍後將探討這些方法。

1.1 如何將現有的回調 API 轉換為 Promise?

我們可以使用 Promise 構造函數將回調轉換為 Promise。

Promise 構造函數接受一個回調,帶有兩個參數resolve和reject。

構造函數立即返回一個對象,即 Promise 實例。當在 promise 實例中使用.then方法時,可以在Promise 「完成」 時得到通知。讓我們來看一個例子。

Promise 僅僅只是回調?

並不是。承諾不僅僅是回調,但它們確實對.then和.catch方法使用了異步回調。Promise 是回調之上的抽象,我們可以連結多個異步操作並更優雅地處理錯誤。來看看它的實際效果。

Promise  反面模式(Promises 地獄)
a(() => {
  b(() => {
    c(() => {
      d(() => {
        // and so on ...
      });
    });
  });
});

不要將上面的回調轉成下面的 Promise 形式:

a().then(() => {
  return b().then(() => {
    return c().then(() => {
      return d().then(() =>{
        // ⚠️ Please never ever do to this! ⚠️
      });
    });
  });
});

上面的轉成,也形成了 Promise 地獄,千萬不要這麼轉。相反,下面這樣做會好點:

a()
  .then(b)
  .then(c)
  .then(d)

超時

你認為以下程序的輸出的是什麼?

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('time is up ⏰');
  }, 1e3);

  setTimeout(() => {
    reject('Oops 🔥');
  }, 2e3);
});

promise
  .then(console.log)
  .catch(console.error);

是輸出:

time is up ⏰
Oops! 🔥

還是輸出:

time is up ⏰

是後者,因為當一個Promise resolved 後,它就不能再被rejected。

一旦你調用一種方法(resolve 或reject),另一種方法就會失效,因為 promise 處於穩定狀態。讓我們探索一個 promise 的所有不同狀態。

1.2 Promise 狀態

Promise 可以分為四個狀態:

⏳ Pending:初始狀態,異步操作仍在進行中。✅ Fulfilled:操作成功,它調用.then回調,例如.then(onSuccess)。⛔️ Rejected: 操作失敗,它調用.catch或.then的第二個參數(如果有)。例如.catch(onError)或.then(..., onError)。😵 Settled:這是 promise 的最終狀態。promise 已經死亡了,沒有別的辦法可以解決或拒絕了。.finally方法被調用。

1.3 Promise 實例方法

Promise API 公開了三個主要方法:then,catch和finally。我們逐一配合事例探討一下。

Promise then

then方法可以讓異步操作成功或失敗時得到通知。它包含兩個參數,一個用於成功執行,另一個則在發生錯誤時使用。

promise.then(onSuccess, onError);

你還可以使用catch來處理錯誤:

promise.then(onSuccess).catch(onError);

Promise 鏈

then 返回一個新的 Promise ,這樣就可以將多個Promise 連結在一起。就像下面的例子一樣:

Promise.resolve()
  .then(() => console.log('then#1'))
  .then(() => console.log('then#2'))
  .then(() => console.log('then#3'));

Promise.resolve立即將Promise 視為成功。因此,以下所有內容都將被調用。輸出將是

then#1
then#2
then#3

Promise catch

Promise  .catch方法將函數作為參數處理錯誤。如果沒有出錯,則永遠不會調用catch方法。

假設我們有以下承諾:1秒後解析或拒絕並列印出它們的字母。

const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 1e3));
const c = () => new Promise((resolve, reject) => setTimeout(() => { console.log('c'), reject('Oops!') }, 1e3));
const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 1e3));

請注意,c使用reject('Oops!')模擬了拒絕。

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)

輸出如下:


在這種情況下,可以看到a,b和c上的錯誤消息。

我們可以使用then函數的第二個參數來處理錯誤。但是,請注意,catch將不再執行。

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d, () => console.log('c errored out but no big deal'))
  .catch(console.error)

由於我們正在處理 .then(..., onError)部分的錯誤,因此未調用catch。d不會被調用。如果要忽略錯誤並繼續執行Promise鏈,可以在c上添加一個catch。像這樣:

Promise.resolve()
  .then(a)
  .then(b)
  .then(() => c().catch(() => console.log('error ignored')))
  .then(d)
  .catch(console.error)


當然,這種過早的捕獲錯誤是不太好的,因為容易在調試過程中忽略一些潛在的問題。

Promise finally

finally方法只在 Promise 狀態是 settled 時才會調用。

如果你希望一段代碼即使出現錯誤始終都需要執行,那麼可以在.catch之後使用.then。

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .then(() => console.log('always called'));

或者可以使用.finally關鍵字:

Promise.resolve()
  .then(a)
  .then(b)
  .then(c)
  .then(d)
  .catch(console.error)
  .finally(() => console.log('always called'));

1.4 Promise 類方法

我們可以直接使用 Promise 對象中四種靜態方法。

Promise.resolve 和  Promise.reject

這兩個是幫助函數,可以讓 Promise 立即解決或拒絕。可以傳遞一個參數,作為下次 .then 的接收:

Promise.resolve('Yay!!!')
  .then(console.log)
  .catch(console.error)

上面會輸出 Yay!!!

Promise.reject('Oops 🔥')
  .then(console.log)
  .catch(console.error)

使用 Promise.all 並行執行多個 Promise

通常,Promise 是一個接一個地依次執行的,但是你也可以並行使用它們。

假設是從兩個不同的api中輪詢數據。如果它們不相關,我們可以使用Promise.all()同時觸發這兩個請求。

在此示例中,主要功能是將美元轉換為歐元,我們有兩個獨立的 API 調用。一種用於BTC/USD,另一種用於獲得EUR/USD。如你所料,兩個 API 調用都可以並行調用。但是,我們需要一種方法來知道何時同時完成最終價格的計算。我們可以使用Promise.all,它通常在啟動多個異步任務並發運行並為其結果創建承諾之後使用,以便人們可以等待所有任務完成。

const axios = require('axios');

const bitcoinPromise = axios.get('https://api.coinpaprika.com/v1/coins/btc-bitcoin/markets');
const dollarPromise = axios.get('https://api.exchangeratesapi.io/latest?base=USD');
const currency = 'EUR';

// Get the price of bitcoins on
Promise.all([bitcoinPromise, dollarPromise])
  .then(([bitcoinMarkets, dollarExchanges]) => {
    const byCoinbaseBtc = d => d.exchange_id === 'coinbase-pro' && d.pair === 'BTC/USD';
    const coinbaseBtc = bitcoinMarkets.data.find(byCoinbaseBtc)
    const coinbaseBtcInUsd = coinbaseBtc.quotes.USD.price;
    const rate = dollarExchanges.data.rates[currency];
    return rate * coinbaseBtcInUsd;
  })
  .then(price => console.log(`The Bitcoin in ${currency} is ${price.toLocaleString()}`))
  .catch(console.log);

如你所見,Promise.all接受了一系列的 Promises。當兩個請求的請求都完成後,我們就可以計算價格了。

我們再舉一個例子:

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.all');
Promise.all([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.all'));

解決這些 Promise 要花多長時間?5秒?1秒?還是2秒?

這個留給你們自己驗證咯。

Promise race

Promise.race(iterable) 方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。

const a = () => new Promise((resolve) => setTimeout(() => resolve('a'), 2000));
const b = () => new Promise((resolve) => setTimeout(() => resolve('b'), 1000));
const c = () => new Promise((resolve) => setTimeout(() => resolve('c'), 1000));
const d = () => new Promise((resolve) => setTimeout(() => resolve('d'), 1000));

console.time('promise.race');
Promise.race([a(), b(), c(), d()])
  .then(results => console.log(`Done! ${results}`))
  .catch(console.error)
  .finally(() => console.timeEnd('promise.race'));

輸出是什麼?

輸出 b。使用 Promise.race,最先執行完成就會結果最後的返回結果。

你可能會問:Promise.race的用途是什麼?

我沒胡經常使用它。但是,在某些情況下,它可以派上用場,比如計時請求或批量處理請求數組。

Promise.race([
  fetch('http://slowwly.robertomurray.co.uk/delay/3000/url/https://api.jsonbin.io/b/5d1fb4dd138da811182c69af'),
  new Promise((resolve, reject) => setTimeout(() => reject(new Error('request timeout')), 1000))
])
.then(console.log)
.catch(console.error);

如果請求足夠快,那麼就會得到請求的結果。

1.5 Promise 常見問題串行執行 promise 並傳遞參數

這次,我們將對Node的fs使用promises API,並將兩個文件連接起來:

const fs = require('fs').promises; // requires node v8+

fs.readFile('file.txt', 'utf8')
  .then(content1 => fs.writeFile('output.txt', content1))
  .then(() => fs.readFile('file2.txt', 'utf8'))
  .then(content2 => fs.writeFile('output.txt', content2, { flag: 'a+' }))
  .catch(error => console.log(error));

在此示例中,我們讀取文件1並將其寫入output 文件。稍後,我們讀取文件2並將其再次附加到output文件。如你所見,writeFile promise返回文件的內容,你可以在下一個then子句中使用它。

如何連結多個條件承諾?

你可能想要跳過 Promise 鏈上的特定步驟。有兩種方法可以做到這一點。

const a = () => new Promise((resolve) => setTimeout(() => { console.log('a'), resolve() }, 1e3));
const b = () => new Promise((resolve) => setTimeout(() => { console.log('b'), resolve() }, 2e3));
const c = () => new Promise((resolve) => setTimeout(() => { console.log('c'), resolve() }, 3e3));
const d = () => new Promise((resolve) => setTimeout(() => { console.log('d'), resolve() }, 4e3));

const shouldExecA = true;
const shouldExecB = false;
const shouldExecC = false;
const shouldExecD = true;

Promise.resolve()
  .then(() => shouldExecA && a())
  .then(() => shouldExecB && b())
  .then(() => shouldExecC && c())
  .then(() => shouldExecD && d())
  .then(() => console.log('done'))

如果你運行該代碼示例,你會注意到只有a和d被按預期執行。

另一種方法是創建一個鏈,然後僅在以下情況下添加它們:

const chain = Promise.resolve();

if (shouldExecA) chain = chain.then(a);
if (shouldExecB) chain = chain.then(b);
if (shouldExecC) chain = chain.then(c);
if (shouldExecD) chain = chain.then(d);

chain
  .then(() => console.log('done'));

如何限制並行 Promise?

要做到這一點,我們需要以某種方式限制Promise.all。

假設你有許多並發請求要執行。如果使用 Promise.all 是不好的(特別是在API受到速率限制時)。因此,我們需要一個方法來限制 Promise 個數, 我們稱其為promiseAllThrottled。

// simulate 10 async tasks that takes 5 seconds to complete.
const requests = Array(10)
  .fill()
  .map((_, i) => () => new Promise((resolve => setTimeout(() => { console.log(`exec'ing task #${i}`), resolve(`task #${i}`); }, 5000))));

promiseAllThrottled(requests, { concurrency: 3 })
  .then(console.log)
  .catch(error => console.error('Oops something went wrong', error));

輸出應該是這樣的:


以上代碼將並發限制為並行執行的3個任務。

實現promiseAllThrottled 一種方法是使用Promise.race來限制給定時間的活動任務數量。

/**
 * Similar to Promise.all but a concurrency limit
 *
 * @param {Array} iterable Array of functions that returns a promise
 * @param {Object} concurrency max number of parallel promises running
 */
function promiseAllThrottled(iterable, { concurrency = 3 } = {}) {
  const promises = [];

  function enqueue(current = 0, queue = []) {
    // return if done
    if (current === iterable.length) { return Promise.resolve(); }
    // take one promise from collection
    const promise = iterable[current];
    const activatedPromise = promise();
    // add promise to the final result array
    promises.push(activatedPromise);
    // add current activated promise to queue and remove it when done
    const autoRemovePromise = activatedPromise.then(() => {
      // remove promise from the queue when done
      return queue.splice(queue.indexOf(autoRemovePromise), 1);
    });
    // add promise to the queue
    queue.push(autoRemovePromise);

    // if queue length >= concurrency, wait for one promise to finish before adding more.
    const readyForMore = queue.length < concurrency ? Promise.resolve() : Promise.race(queue);
    return readyForMore.then(() => enqueue(current + 1, queue));
  }

  return enqueue()
    .then(() => Promise.all(promises));
}

promiseAllThrottled一對一地處理 Promises 。它執行Promises並將其添加到隊列中。如果隊列小於並發限制,它將繼續添加到隊列中。達到限制後,我們使用Promise.race等待一個承諾完成,因此可以將其替換為新的承諾。這裡的技巧是,promise 自動完成後會自動從隊列中刪除。另外,我們使用 race 來檢測promise 何時完成,並添加新的 promise 。

人才們的 【三連】 就是小智不斷分享的最大動力,如果本篇博客有任何錯誤和建議,歡迎人才們留言,最後,謝謝大家的觀看。

作者:Adrian Mejia  譯者:前端小智  來源:adrianmjia

原文:https://adrianmejia.com/promises-tutorial-concurrency-in-javascript-node/

活動推薦:

極客時間「 重學前端 」將帶你擺脫「土方法」,手把手重塑前端邏輯,新人首單僅¥19.9!


相關焦點

  • JavaScript之Promise對象
    javascript是單線程語言,所以都是同步執行的,要實現異步就得通過回調函數的方式,但是過多的回調會導致回調地獄,代碼既不美觀,也不易維護,所以就有了promise;Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。
  • JavaScript 異步與 Promise 實現
    在閱讀本文之前,你應該已經了解JavaScript異步實現的幾種方式:回調函數,發布訂閱模式,Promise,生成器(Generator),其實還有
  • JavaScript Promise啟示錄
    翻譯成代碼類似:var resB = B();var runA = function() { resB.then(execS1, execS2);};runA();只看上面這行代碼,好像看不出什麼特別之處
  • 關於JavaScript錯誤處理最完整的指南
    當我們在瀏覽器中做蠢事時它們就會被拋出,例如:document.body.appendChild(document.cloneNode(true));Uncaught DOMException: Node.appendChild: May not add a Document as a child有關完整列表
  • 從JavaScript的事件循環到Promise
    這種情況下,參與異步過程的其實有2個線程主體,一個是javascript的主線程,另一個是瀏覽器線程。熟悉Javascript的人都知道,Javascript內部有一個事件隊列,所有的處理方法都在這個隊列中,Javascript主線程就是輪詢這個隊列處理,這個好處是使得CPU永遠是忙碌狀態。
  • JavaScript異步與Promise實現
    本文已獲熊建剛授權分享,文章篇幅較長,還請耐心觀看在閱讀本文之前,你應該已經了解JavaScript異步實現的幾種方式:回調函數,
  • 【JavaScript】Promise函數的用法
    javascript
  • 12 個 GitHub 上超火的 JavaScript 奇技淫巧項目,找到寫 JavaScript 的靈感!
    它不是必備,但在未來學習(JavaScript)中,可以作為一篇指南。然後,當我們使用 === 操作符時,兩者的值以及類型都應該是相同的。new Number() 是一個對象而不是 number,因此返回 false。https://github.com/lydiahallie/javascript-questions4. JavaScript 30
  • 從promise讀懂JavaScript異步編程
    而setTimeout和node.js中的readFile等函數就是異步執行的,如下圖就是兩個典型的回調函數,因為readFile是node.js中相應的方法,此時應該按照node.js中的執行機制執行。
  • JavaScript錯誤處理完全指南
    這意味著我們可以偵聽頁面中任何 HTML 元素上的事件:https://www.valentinog.com/blog/event/#how-does-event-driven-applies-to-javascript-in-the-browser(Node.js 會在未來版本中支持 EventTarget)。
  • javascript入門推薦書籍-授人以魚不如授人以漁
    我們應該把 javascript 當作一門真正的程式語言,而不是玩具語言。2、JavaScript權威指南當然,作為入門書的話《JavaScript權威指南(第5版)》也非常強大(這名字可不是白起),網上關於此書的評價很多,意思大概都是說這書就是一個JS的文檔手冊,如果你有閒錢,並且習慣翻 書查詢,那麼就來一本吧。
  • 如何正確學習JavaScript
    原文:http://javascriptissexy.com/how-to-learn-JavaScript-properly/翻譯:Jaward華仔目錄既然你找到這篇文章來1、閱讀《JavaScript權威指南》第7~8章或者《JavaScript高級程序設計》第5和7章。2、此時,你應該花大量時間在瀏覽器控制臺上寫代碼,測試if-else語句,for循環,數組,函數,對象等等。更重要的是,你要鍛鍊和掌握獨立寫代碼,不用藉助Codecademy。在Codecademy上做題時,每個任務對你來說應該都很簡單,不需要點幫助和提示。
  • 使用Promise模式來簡化JavaScript的異步回調
    var showMsg = function(){// 構造promise實例var promise = new E.Promise();setTimeout(function(){alert( 『hello』 );// 改變promise的狀態promise.resolve(
  • 理解異步之美--- Promise與async await(一)
    在你不知道的javascript一書中,對於回調的信任問題做了闡述當你使用第三方的庫的方法處理回調時很有可能遇到以下信任內容:怎麼解決???? 這種信任問題該怎麼辦?在javascript中這樣的人就是Promise。Promise的實例有三個狀態,Pending(進行中)、Resolved(已完成)、Rejected(已拒絕)。當你把一件事情交給promise時,它的狀態就是Pending,任務完成了狀態就變成了Resolved、沒有完成失敗了就變成了Rejected。
  • 關於js中的promise,與其說是一種語法還不如說是一種思想!
    在程式設計師群體中,存在著好多種鄙視鏈,其中一種鄙視鏈就是語言鄙視鏈了,大多數後端程式設計師都比較小瞧javascript這門語言了,但我個人認為即便如此,也不影響javascript的偉大,js作為一門前端語言,除了不能連接資料庫之外,有好多別致的語法,比如說js中的閉包,雖然被大多數人吐糟
  • JavaScript 執行機制
    1.關於javascriptjavascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。所以一切javascript版的"多線程"都是用單線程模擬出來的,一切javascript多線程都是紙老虎!
  • 前端Javascript進階-ES6中Promise對象
    new(2)參數是一個函數【1】函數裡面是操作成功調用第一個resolve函數參數(pending->fulfilled狀態)【2】函數裡面是操作失敗調第二個用reject回調函數(pending->rejected狀態)let promise
  • 面試官:為什麼 Promise 比setTimeout() 快?
    promise.resolve(1)是一個靜態函數,它返回一個立即解析的promise。setTimeout(callback, 0)以0毫秒的延遲執行回調函數。我們可以看到先列印'Resolved!',再列印Timeout completed!
  • Promise & Generator――幸福地用同步方法寫異步JavaScript
    回過神來的時候被自己嚇了一跳,這可不行啊,醜得沒法看啊!於是打算嘗試一下一些流行的異步的解決方案。經過一番折騰之後...我終於找到了一個令自己滿意的方案了(愛不釋手)。不過在正式介紹它之前先扯一些其他的相關知識先吧! 1.
  • Promise 初使用
    promise初使用promise是es6是新增的構造器,用來提供另一種異步代碼的實現方案。