我们都经历过。我们有大量数据,需要为每个条目发出某种 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中文网其他相关文章!