這篇文章算是 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。
- 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.all
- Promise.reject
- Promise.resolve
- Promise.race
Promise.resolve 和 Promise.reject
這兩個是幫助函數,可以讓 Promise 立即解決或拒絕??梢詡鬟f一個參數,作為下次 .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/