JavaScript Promise 完整指南

做者:Adrian Mejia
譯者:前端小智
來源:adrianmjia
點贊再看,微信搜索 大遷世界,B站關注【 前端小智】這個沒有大廠背景,但有着一股向上積極心態人。本文 GitHub https://github.com/qq44924588... 上已經收錄,文章的已分類,也整理了不少個人文檔,和教程資料。

最近開源了一個 Vue 組件,還不夠完善,歡迎你們來一塊兒完善它,也但願你們能給個 star 支持一下,謝謝各位了。javascript

github 地址:https://github.com/qq44924588...前端

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

1.JavaScript Promises

Promise 是一個容許咱們處理異步操做的對象,它是 es5 早期回調的替代方法。java

與回調相比,Promise 具備許多優勢,例如:node

  • 讓異步代碼更易於閱讀。
  • 提供組合錯誤處理。

* 更好的流程控制,可讓異步並行或串行執行。ios

回調更容易造成深度嵌套的結構(也稱爲回調地獄)。 以下所示:git

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

若是將這些函數轉換爲 Promise,則能夠將它們連接起來以生成更可維護的代碼。 像這樣:github

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

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

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

咱們可使用 Promise 構造函數將回調轉換爲 Promise。axios

Promise 構造函數接受一個回調,帶有兩個參數resolvereject

  • 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

一旦你調用一種方法(resolvereject),另外一種方法就會失效,由於 promise 處於穩定狀態。 讓咱們探索一個 promise 的全部不一樣狀態。

1.2 Promise 狀態

Promise 能夠分爲四個狀態:

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

clipboard.png

你們都說簡歷沒項目寫,我就幫你們找了一個項目,還附贈【搭建教程】

1.3 Promise 實例方法

Promise API 公開了三個主要方法:thencatchfinally。 咱們逐一配合事例探討一下。

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)

輸出以下:

圖片描述

在這種狀況下,能夠看到abc上的錯誤消息。

咱們可使用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)部分的錯誤,所以未調用catchd不會被調用。 若是要忽略錯誤並繼續執行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.all
  • Promise.reject
  • Promise.resolve
  • Promise.race

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'))

若是你運行該代碼示例,你會注意到只有ad被按預期執行。

另外一種方法是建立一個鏈,而後僅在如下狀況下添加它們:

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 。

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


代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:https://adrianmejia.com/promi...

交流

文章每週持續更新,能夠微信搜索 【大遷世界 】 第一時間閱讀,回覆 【福利】 有多份前端視頻等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已經收錄,歡迎Star。

相關文章
相關標籤/搜索