我們都經歷過。我們有大量數據,需要為每個條目發出某種 api 請求。假設它是不同場地的 id 數組,您需要從中取得場地提供者並傳回該提供者數組。我們建立了一個新函數來發出這些請求...
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); for (let i = 0; i >= idArray.length - 1; i++) { const res = await fetch( `https://venues_for_me.org/venueid=${idArray[i]}` ); const venue = res.data; providers[i] = venue.provider; } return providers; };
哎呀,你剛剛根據你的所有請求關閉了 8 年前的遺留伺服器......
我覺得我們都曾在某些時候犯過這樣的錯誤,一個解決方案是在一批請求之間設定幾毫秒的超時...
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
寫完這個例子我就想洗個澡……更不用說相同數組的絕對瘋狂的重複(或者凌亂的代碼);這是通過設置任意超時人為限制您的執行速度
這裡一個好的答案是創建一個並發限制器,僅當最大並發數有空間時才創建承諾。類似:
getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
如您所見,為了不失去承諾,您需要實現某種佇列來保留要發出的請求的積壓。這篇文章的標題來了。
我正在觀看 The Primagen 的一段視頻,一個特定的部分引起了我的注意。在 Netflix 訪談中,他最喜歡問的問題之一是讓受訪者創建一個非同步隊列,以及執行 Promise 的最大並發數。
這聽起來和我遇到的上述問題一模一樣!
這個面試問題有多個層次。佇列實作後,實作錯誤重試。
我花了一個下午的時間來應對這個挑戰,很快我就發現我有技能問題。事實證明,我並不像我想像的那樣了解 Promise。
在花了幾天時間深入研究承諾之後,中止控制器、地圖、集合、弱地圖和集合。我創建了 Asyncrify
使用 Asyncrify 我的目標很簡單。建立另一個非同步隊列。但沒有外部依賴,資源盡可能輕量。
它需要能夠向隊列添加函數,並設定最大並發度。設定和處理逾時並啟用、停用指數下降的重試。
那我聽到你沒有問的那些技能問題是什麼?
了解你的承諾這一點我怎麼強調都不為過。
我遇到的第一個問題是我不明白 Promise 的執行是如何運作的。我的第一個實作看起來像這樣:
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); for (let i = 0; i >= idArray.length - 1; i++) { const res = await fetch( `https://venues_for_me.org/venueid=${idArray[i]}` ); const venue = res.data; providers[i] = venue.provider; } return providers; };
我確信您立即看到了問題。我正在使用 Promise.race 同時執行我的「最大並發」承諾。
但這只有在第一個承諾兌現後才會繼續。其餘的被忽略。然後我再添加 1 個並再次執行它們。
我必須回到基礎。
解決方案是改用 .then 和 .catch 並僅噹噹前運行的部分中有空位時才運行該函數。
const getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
現在我們可以更好地追蹤並發承諾,但我們也使用戶能夠按照他們想要的方式處理錯誤和解決方案。
請使用中止控制器我經常看到的一個大錯誤是,當承諾在初始化後不再需要時,人們不會使用中止控制器。我也做了這個。
一開始,為了實現超時,我使用了Promise.race
getProvidersFromVenueIDs = async (idArray) => { const providers = Array(idArray.length); const batchSize = 50; for (let i = 0; i >= idArray.length - 1; i++) { const batchToExecute = Array(batchSize); for (let y = 1; i >= batchSize; i++) { batchToExecute[i] = fetch( `https://venues_for_me.org/venue?id=${idArray[i]}`, ); await (async () => setTimeout(() => {}, 200))(); } const responses = await Promise.all(batchToExecute); responses.forEach((venue) => { providers[i] = venue.provider; }); } return providers; };
正如你想像的那樣。逾時後承諾仍會執行。它只是被忽略了。這看起來很像我在實作佇列時犯的第一個錯誤,不是嗎?
我對中止控制器做了一些研究,因為我對它們的唯一經驗只是反應。
中止訊號.超時!這正是我想做的事!
我的程式碼唯一的更新是 1 行
async #runTasksRecursively() { await this.#runAsync(); if (this.#queue.size === 0 && this.#retries.length === 0) { return; } this.#addToPromiseBlock(); } async #runAsync() { if (!this.#runningBlock.every((item) => item === undefined)) { await Promise.race(this.#runningBlock); } } #addToPromiseBlock() { const emptyspot = this.#getEmptySpot(); if (this.#retries.length > 0 && !this.#lastRunWasError) { console.log(this.#retries); if (this.#errorsToInject.size > 0) { const task = this.#popInSet(this.#errorsToInject); if (this.#queue.size !== 0) { this.#lastRunWasError = true; } this.#assignPromisToExecutionArray(task, emptyspot); } } else { const task = this.#popInSet(this.#queue); this.#lastRunWasError = false; this.#assignPromisToExecutionArray(task, emptyspot); } }
哇,太簡單了!但現在套件的用戶需要創建樣板才能使用超時功能。無需害怕!我是為了你才這麼做的!
add(fn, callback, errCallback) { if (this.#maxConcurrency !== 0 && this.#running >= this.#maxConcurrency) { this.#queue.add(fn); } else { this.#running++; fn() .then(callback) .catch(errCallback) .finally(() => { this.#running--; if (this.#queue.size > 0) { const nextPromise = this.#queue.values().next().value; this.#queue.delete(nextPromise); this.add(nextPromise, callback, errorCallback); } }); } }
那麼要如何使用 Asyncrify 呢?
嗯,這真的很容易。我們首先建立隊列。
#promiseBuilder(fn) { const promise = new Array(this.#promiseTimeout > 0 ? 2 : 1); promise[0] = fn(); if (this.#promiseTimeout > 0) { promise[1] = this.#timeoutHandler(); } return promise; } #promiseRunner(fn, callback) { const promise = this.#promiseBuilder(fn); Promise.race(promise) .then((res) => { callback(res, null); }) .catch((err) => { this.#errorHandler(err, fn, callback); }) .finally(() => { this.#running--; this.#runPromiseFromQueue(callback); }); }
佇列預設沒有逾時或退出,也沒有最大並發數。
您也可以向建構函數提供配置物件。
const promise = fn( this.#timeout > 0 ? AbortSignal.timeout(this.#timeout) : null, );
要將 Promise 加入到佇列中,您必須包裝在傳回它的函數中。
export const abortHandler = (signal, reject) => { if (signal.aborted) { return reject(new Error("Aborted")); } const abortHandler = () => { reject(new Error("Aborted")); signal.removeEventListener("abort", abortHandler); }; signal.addEventListener("abort", abortHandler); };
記得加入中止處理程序才能使用逾時功能!
然後您需要做的就是將函數以及回調和錯誤回調傳遞給 add 方法
import Queue from 'Asyncrify' const queue = new Queue()
添加就是這樣!想添加多少就添加多少,想快多少就添加多少,一次只會運行 3 個,直到全部完成為止!
在創建這個包的過程中我學到了很多。可以說我很久以前就該知道的事。這就是我寫這篇文章的原因。我希望你們看到我犯過的那些可以說是愚蠢的錯誤,並且受到鼓勵去犯愚蠢的錯誤並從中學習。而不是在事情發生時感到尷尬並躲開。
出去寫一篇文章。創建一個微型包,每週從機器人下載 10 次。你最終會學到你從來不知道自己需要的東西
以上是Netflix 面試問題如何變成我的第一個 NPM 包的詳細內容。更多資訊請關注PHP中文網其他相關文章!