做者:Adrian Mejia
譯者:前端小智
來源:adrianmjia
點贊再看,微信搜索
【大遷世界】,B站關注【
前端小智】這個沒有大廠背景,但有着一股向上積極心態人。本文
GitHub
https://github.com/qq44924588... 上已經收錄,文章的已分類,也整理了不少個人文檔,和教程資料。
最近開源了一個 Vue 組件,還不夠完善,歡迎你們來一塊兒完善它,也但願你們能給個 star 支持一下,謝謝各位了。javascript
github 地址:https://github.com/qq44924588...前端
這篇文章算是 JavaScript Promises 比較全面的教程,該文介紹了必要的方法,例如 then
,catch
和finally
。 此外,還包括處理更復雜的狀況,例如與Promise.all
並行執行Promise
,經過Promise.race
來處理請求超時的狀況,Promise 鏈以及一些最佳實踐和常見的陷阱。vue
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
咱們可使用 Promise 構造函數將回調轉換爲 Promise。axios
Promise 構造函數接受一個回調,帶有兩個參數resolve
和reject
。
構造函數當即返回一個對象,即 Promise
實例。 當在 promise 實例中使用.then
方法時,能夠在Promise 「完成」 時獲得通知。 讓咱們來看一個例子。
並非。承諾不只僅是回調,但它們確實對.then
和.catch
方法使用了異步回調。 Promise 是回調之上的抽象,咱們能夠連接多個異步操做並更優雅地處理錯誤。來看看它的實際效果。
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
的全部不一樣狀態。
Promise 能夠分爲四個狀態:
.then
回調,例如.then(onSuccess)
。.catch
或.then
的第二個參數(若是有)。 例如.catch(onError)
或.then(..., onError)
。.finally
方法被調用。你們都說簡歷沒項目寫,我就幫你們找了一個項目,還附贈【搭建教程】。
Promise API 公開了三個主要方法:then
,catch
和finally
。 咱們逐一配合事例探討一下。
then
方法可讓異步操做成功或失敗時獲得通知。 它包含兩個參數,一個用於成功執行,另外一個則在發生錯誤時使用。
promise.then(onSuccess, onError);
你還可使用catch
來處理錯誤:
promise.then(onSuccess).catch(onError);
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
方法將函數做爲參數處理錯誤。 若是沒有出錯,則永遠不會調用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)
固然,這種過早的捕獲錯誤是不太好的,由於容易在調試過程當中忽略一些潛在的問題。
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'));
咱們能夠直接使用 Promise
對象中四種靜態方法。
這兩個是幫助函數,可讓 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(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);
若是請求足夠快,那麼就會獲得請求的結果。
你們都說簡歷沒項目寫,我就幫你們找了一個項目,還附贈【搭建教程】。
此次,咱們將對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.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。