当前位置: 首页 > news >正文

Promise.all 速查与扩展实战

Promise.all 速查与扩展实战

面向实战的 Promise.all 指南:核心概念、常见陷阱、性能与并发控制、超时与取消、重试与进度、以及与其“姐妹方法”的对比与选型。


一、核心认识(必须知道)

  • 作用:并行等待一组可迭代(数组/类数组)的值,所有都 fulfilled 才整体 fulfilled;任意一个 rejected 就立即整体 rejected(短路)。

  • 顺序:返回结果的顺序与输入顺序一致,和每个子任务完成的先后无关。

  • 入参宽松:入参元素不是 Promise 也会被隐式 Promise.resolve

  • 空数组:await Promise.all([]) 立即得到 []

示例:


const [user, posts] = await Promise.all([fetch('/api/user/42').then((r) => r.json()),fetch('/api/posts?user=42').then((r) => r.json()),]);

错误传播(短路):


try {await Promise.all([fetch('/ok'),Promise.reject(new Error('boom')), // 将导致整体立即 reject]);} catch (err) {console.error('首个错误:', err);}

二、与 async/await 的正确搭配

  • 并行:把 promise 先“都发出去”,再 await Promise.all([...])

  • 避免串行:for...of + await 会顺序执行,慢且常见性能陷阱。


// 推荐:并行const urls = ['a.json', 'b.json', 'c.json'];const tasks = urls.map((u) => fetch(u).then((r) => r.json()));const results = await Promise.all(tasks);// 不推荐:串行for (const u of urls) {const r = await fetch(u); // 每次等上一次完成// ...}

三、错误收集与“全部结果”

  • 需求:不短路,拿到所有成功/失败结果 → 用 Promise.allSettled

const results = await Promise.allSettled([fetch('/a'),fetch('/b'),fetch('/c'),]);const fulfilled = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);const rejected = results.filter((r) => r.status === 'rejected').map((r) => r.reason);
  • 也可“包一层”把错误转为普通值收集:

const wrap = (p) =>p.then((v) => ({ ok: true, value: v }),(e) => ({ ok: false, reason: e }));const settled = await Promise.all([p1, p2, p3].map(wrap));

提示:只有 Promise.any 在全部失败时才会抛出 AggregateErrorPromise.all 不会。


四、并发限制(防压垮服务)

当列表很大时,同时全开可能打爆后端或命中浏览器连接上限。实现一个 allLimit


function pLimit(concurrency) {const queue = [];let active = 0;const next = () => {active--;if (queue.length) queue.shift()();};const run = async (fn, resolve, reject) => {active++;try {resolve(await fn());} catch (e) {reject(e);} finally {next();}};return (fn) =>new Promise((resolve, reject) => {const task = () => run(fn, resolve, reject);active < concurrency ? task() : queue.push(task);});}async function allLimit(fns, limit) {// fns: (() => Promise<T>)[]const limitFn = pLimit(limit);return Promise.all(fns.map((fn) => limitFn(fn)));}// 用法const urls = Array.from({ length: 50 }, (_, i) => `/api/item/${i}`);const fns = urls.map((u) => () => fetch(u).then((r) => r.json()));const data = await allLimit(fns, 5); // 最多 5 个并发

五、超时与取消(防长时间悬挂)

  • 通用超时包装:

function withTimeout(promise, ms, msg = 'Timeout') {let timer;return Promise.race([promise.finally(() => clearTimeout(timer)),new Promise((_, reject) => {timer = setTimeout(() => reject(new Error(msg)), ms);}),]);}// 用法await withTimeout(fetch('/slow'), 5000);
  • fetch 友好取消(AbortController):

async function fetchWithTimeout(url, ms) {const ctrl = new AbortController();const id = setTimeout(() => ctrl.abort('timeout'), ms);try {const res = await fetch(url, { signal: ctrl.signal });return res;} finally {clearTimeout(id);}}const [a, b] = await Promise.all([fetchWithTimeout('/a', 3000),fetchWithTimeout('/b', 3000),]);

六、重试与退避(提高成功率)


async function retry(fn, { retries = 3, min = 200, factor = 2 } = {}) {let attempt = 0,delay = min;while (true) {try {return await fn();} catch (e) {if (attempt++ >= retries) throw e;await new Promise((r) => setTimeout(r, delay));delay *= factor; // 指数退避}}}// 搭配并发限制const tasks = urls.map((u) => () => retry(() => fetch(u).then((r) => r.json())));const results = await allLimit(tasks, 4);

七、姐妹方法选型对比

  • Promise.all:全部成功才成功;任意失败立即失败;结果顺序与输入一致。

  • Promise.allSettled:永不短路,返回每个的状态与值/原因。

  • Promise.any:第一个成功就成功;全部失败才失败(AggregateError)。

  • Promise.race:第一个 settle(成功或失败)就返回。

常见用法:


// 取最快可用的镜像源const res = await Promise.any([fetch('https://cdn1.example.com/a'),fetch('https://cdn2.example.com/a'),fetch('https://cdn3.example.com/a'),]);// 竞速 + 超时await Promise.race([fetch('/maybe-slow'),new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 3000)),]);

八、TypeScript 类型友好用法

元组精确推断:


const [user, count] = await Promise.all([getUser(id), getCount(id)] as const); // as const 让 TS 推断为定长元组

通用帮助:


function all<T extends readonly unknown[]>(values: T): Promise<{ [K in keyof T]: Awaited<T[K]> }> {return Promise.all(values) as any;}const [a, b, c] = await all([Promise.resolve(1),Promise.resolve('x'),Promise.resolve({ k: 1 }),] as const);

九、对象属性并行(allProps)

当你有“对象的值是 Promise”,可把它们并行并还原为对象:


async function allProps(obj) {const entries = await Promise.all(Object.entries(obj).map(async ([k, v]) => [k, await v]));return Object.fromEntries(entries);}// 用法const result = await allProps({profile: fetch('/me').then((r) => r.json()),notices: fetch('/notices').then((r) => r.json()),});

十、进度反馈(用户体验)


async function progressAll(promises, onProgress) {let done = 0;const wrap = (p) =>Promise.resolve(p).finally(() => onProgress(++done, promises.length));return Promise.all(promises.map(wrap));}// 用法await progressAll(tasks, (done, total) => {console.log(`已完成 ${done}/${total}`);});

十一、实用场景片段

  • 并行加载图片:

const loadImg = (src) =>new Promise((res, rej) => {const img = new Image();img.onload = () => res(img);img.onerror = rej;img.src = src;});const imgs = await Promise.all(urls.map(loadImg));
  • Node.js 并行读文件:

import { readFile } from 'node:fs/promises';const [a, b, c] = await Promise.all([readFile('a.txt', 'utf8'),readFile('b.txt', 'utf8'),readFile('c.txt', 'utf8'),]);
  • 分批处理(批大小 = 并发限制):

async function batch(items, size, worker) {const out = [];for (let i = 0; i < items.length; i += size) {const slice = items.slice(i, i + size);const r = await Promise.all(slice.map(worker));out.push(...r);}return out;}

十二、常见陷阱与最佳实践

  • forEach + async 无法被 await 等待,用 map + Promise.allfor...of

  • 避免巨大数组一次性全开请求,使用并发限制或分批。

  • 注意早失败短路:若要“尽量多成功”,用 allSettled 或包装错误。

  • 记得捕获错误,避免未处理的 Promise 拒绝。

  • fetch 需要 AbortController 才能真正取消;race 只是在值层“放弃”。


十三、速用清单(Cheatsheet)

  • 并行取多数据:await Promise.all([p1, p2, p3])

  • 收集全部成功/失败:await Promise.allSettled([...])

  • 取第一个成功:await Promise.any([...])

  • 取第一个完成:await Promise.race([...])

  • 并发限制:await allLimit(fns, N)(见上面的实现)

  • 超时:await withTimeout(p, ms);fetch 用 AbortController

  • 重试退避:await retry(fn, { retries, min, factor })

  • 进度:await progressAll(promises, onProgress)

  • 对象属性并行:await allProps(obj)


附:开箱即用的小工具库

已在同目录添加 promise-all-utils.js(UMD),可直接在浏览器或 Node.js/打包器中使用:

  • 浏览器:

    
    <script src="./promise-all-utils.js"></script><script>const { mapWithLimit, retry, withTimeout, allProps, progressAll } =window.PromiseUtils;</script>
  • Node.js (CommonJS):

    
    const {mapWithLimit,retry,withTimeout,allProps,progressAll,} = require('./promise-all-utils');
  • ESM:

    
    import {mapWithLimit,retry,withTimeout,allProps,progressAll,} from './promise-all-utils.js';

常用示例:


import {mapWithLimit,retry,withTimeout,allProps,progressAll,} from './promise-all-utils.js';// 1) 并发限制抓取const urls = ['a.json', 'b.json', 'c.json'];const data = await mapWithLimit(urls, 3, (u) => fetch(u).then((r) => r.json()));// 2) 重试 + 退避const json = await retry(() => fetch('/flaky').then((r) => r.json()), {retries: 4,min: 300,factor: 2,jitter: true,});// 3) 超时await withTimeout(fetch('/slow'), 5000);// 4) 对象属性并行const obj = await allProps({a: fetch('/a').then((r) => r.json()),b: fetch('/b').then((r) => r.json()),});// 5) 进度反馈await progressAll(urls.map((u) => fetch(u)),(done, total) => console.log(`${done}/${total}`));

promise-all-utils.js


/*
Promise Utils (UMD) — Lightweight helpers around Promise.all and friends
Usage:
- Browser: <script src="promise-all-utils.js"></script> then window.PromiseUtils
- Node (CJS): const { mapWithLimit, retry, withTimeout, ... } = require('./promise-all-utils');
- ESM/Bundlers: import { mapWithLimit, retry } from './promise-all-utils.js'
*/
(function (root, factory) {if (typeof define === 'function' && define.amd) {define([], factory);} else if (typeof module === 'object' && module.exports) {module.exports = factory();} else {root.PromiseUtils = factory();}
})(typeof self !== 'undefined' ? self : this, function () {'use strict';/*** Create a concurrency limiter similar to p-limit* @param {number} concurrency > 0* @returns {(fn: () => Promise<any>) => Promise<any>}*/function pLimit(concurrency) {if (!(Number.isInteger(concurrency) && concurrency > 0)) {throw new Error('pLimit: concurrency must be a positive integer');}const queue = [];let active = 0;const next = () => {active--;if (queue.length) queue.shift()();};const run = async (fn, resolve, reject) => {active++;try {resolve(await fn());} catch (e) {reject(e);} finally {next();}};return (fn) =>new Promise((resolve, reject) => {const task = () => run(fn, resolve, reject);active < concurrency ? task() : queue.push(task);});}/*** Limit concurrency for an array of promise factories* @template T* @param {Array<() => Promise<T>>} fns* @param {number} limit* @returns {Promise<T[]>}*/function allLimit(fns, limit) {const limitFn = pLimit(limit);return Promise.all(fns.map((fn) => limitFn(fn)));}/*** Map items with concurrency limit* @template I,O* @param {I[]} items* @param {number} limit* @param {(item: I, index: number) => Promise<O>} mapper* @returns {Promise<O[]>}*/function mapWithLimit(items, limit, mapper) {const limitFn = pLimit(limit);return Promise.all(items.map((it, i) => limitFn(() => mapper(it, i))));}/*** Wrap a promise with a timeout using Promise.race* @template T* @param {Promise<T>} promise* @param {number} ms* @param {string} [message='Timeout']* @returns {Promise<T>}*/function withTimeout(promise, ms, message = 'Timeout') {let timer;const timeout = new Promise((_, reject) => {timer = setTimeout(() => reject(new Error(message)), ms);});return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));}/*** fetch with AbortController-based timeout* @param {RequestInfo} url* @param {number} ms* @param {RequestInit} [options]* @returns {Promise<Response>}*/async function fetchWithTimeout(url, ms, options = {}) {if (typeof fetch !== 'function') {throw new Error('fetchWithTimeout requires global fetch. Provide a polyfill in Node < 18.');}const ctrl = new (typeof AbortController !== 'undefined' ? AbortController : function () {})();if (!ctrl || !ctrl.signal) {// Environment without AbortController; fall back to simple timeout racereturn withTimeout(fetch(url, options), ms, 'Timeout');}const userSignal = options.signal;/** @type {AbortSignal} */let signal = ctrl.signal;// If user provided a signal, try to combineif (userSignal) {if (typeof AbortSignal !== 'undefined' &&typeof AbortSignal.any === 'function') {signal = AbortSignal.any([ctrl.signal, userSignal]);} else {// Fallback: if userSignal aborts first, propagate; cannot truly combine without AbortSignal.anyif (userSignal.aborted) ctrl.abort(userSignal.reason || 'user-abort');elseuserSignal.addEventListener('abort', () =>ctrl.abort(userSignal.reason || 'user-abort'));signal = ctrl.signal;}}const id = setTimeout(() => ctrl.abort('timeout'), ms);try {const res = await fetch(url, { ...options, signal });return res;} finally {clearTimeout(id);}}/*** Retry with exponential backoff and optional jitter* @template T* @param {() => Promise<T>} fn* @param {{*  retries?: number,*  min?: number,*  factor?: number,*  max?: number,*  jitter?: boolean,*  shouldRetry?: (err: any, attempt: number) => boolean,*  onRetry?: (err: any, attempt: number, delayMs: number) => void,* }} [opts]* @returns {Promise<T>}*/async function retry(fn, opts = {}) {const {retries = 3,min = 200,factor = 2,max = Infinity,jitter = true,shouldRetry = (err) => (err && err.name) !== 'AbortError',onRetry,} = opts;let attempt = 0;// First try is attempt 0; allow up to `retries` additional attempts on failurewhile (true) {try {return await fn();} catch (err) {if (!shouldRetry(err, attempt) || attempt >= retries) throw err;let delay = Math.min(min * Math.pow(factor, attempt), max);if (jitter) {const r = 0.5 + Math.random(); // [0.5, 1.5)delay = Math.round(delay * r);}if (typeof onRetry === 'function') onRetry(err, attempt + 1, delay);await new Promise((r) => setTimeout(r, delay));attempt++;}}}/*** Wrap each promise to report progress* @template T* @param {Array<Promise<T>|(() => Promise<T>)>} items* @param {(done: number, total: number) => void} onProgress* @returns {Promise<T[]>}*/function progressAll(items, onProgress) {let done = 0;const total = items.length;const toPromise = (it) => (typeof it === 'function' ? it() : it);const wrap = (p) =>Promise.resolve(p).finally(() => onProgress(++done, total));return Promise.all(items.map((it) => wrap(toPromise(it))));}/*** Await object property promises and reconstruct object* @template T extends Record<string, any>* @param {T} obj* @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>}*/async function allProps(obj) {const entries = await Promise.all(Object.entries(obj).map(async ([k, v]) => [k, await v]));// @ts-ignore: JS runtime is fine; TS users should use .d.ts or JSDocreturn Object.fromEntries(entries);}/*** Process items in batches (batch size ~ concurrency)* @template I,O* @param {I[]} items* @param {number} size* @param {(item: I, index: number) => Promise<O>} worker* @returns {Promise<O[]>}*/async function batch(items, size, worker) {const out = [];for (let i = 0; i < items.length; i += size) {const slice = items.slice(i, i + size);const r = await Promise.all(slice.map((it, j) => worker(it, i + j)));out.push(...r);}return out;}/*** Tuple-friendly all (mainly for TS users; in JS it aliases Promise.all)* @template T extends readonly unknown[]* @param {T} values* @returns {Promise<{ [K in keyof T]: Awaited<T[K]> }>}*/function all(values) {// @ts-ignore - for JS consumers, this is just Promise.allreturn Promise.all(values);}/*** Wrap promise to an object capturing success/failure (never throws)* @template T* @param {Promise<T>} p* @returns {Promise<{ ok: true, value: T } | { ok: false, reason: any }>}*/function settleWrap(p) {return Promise.resolve(p).then((v) => ({ ok: true, value: v }),(e) => ({ ok: false, reason: e }));}return {pLimit,allLimit,mapWithLimit,withTimeout,fetchWithTimeout,retry,progressAll,allProps,batch,all,settleWrap,};
});

在 React/Vue 项目中如何使用

以下示例假设你把 promise-all-utils.js 放到你的项目源码目录(例如 src/utils/promise-all-utils.js),并以 ESM 方式导入。按需调整相对路径。

React(Hooks)

典型需求:在组件挂载时并发拉多份数据,带并发限制、超时、重试与进度反馈;组件卸载时取消请求并避免 setState 内存泄漏。


// src/components/Dashboard.jsximport { useEffect, useState } from 'react';import {mapWithLimit,retry,fetchWithTimeout,progressAll,allProps,} from '../utils/promise-all-utils.js';export default function Dashboard() {const [data, setData] = useState(null);const [loading, setLoading] = useState(true);const [error, setError] = useState(null);const [progress, setProgress] = useState(0);useEffect(() => {let alive = true;const ctrl = new AbortController();(async () => {try {// 示例:对象属性并行 + 数组并发请求混用const base = await allProps({me: fetchWithTimeout('/api/me', 5000, { signal: ctrl.signal }).then((r) => r.json()),settings: fetchWithTimeout('/api/settings', 5000, {signal: ctrl.signal,}).then((r) => r.json()),});const urls = ['/api/list/1','/api/list/2','/api/list/3','/api/list/4',];// 构造带重试和超时的 mapper(并发 3)并汇报进度const tasks = urls.map((u) => () =>retry(() =>fetchWithTimeout(u, 5000, { signal: ctrl.signal }).then((r) =>r.json()),{ retries: 3, min: 300, factor: 2, jitter: true }));const total = tasks.length;let done = 0;const results = await progressAll(tasks.map((fn) => fn()),(d) => {done = d;if (alive) setProgress(Math.round((d / total) * 100));});if (!alive) return;setData({ ...base, lists: results });setLoading(true); // 可选:若你需要进一步并发处理可暂置 truesetLoading(false);} catch (e) {if (!alive || e?.name === 'AbortError') return;setError(e);setLoading(false);}})();// 卸载时取消return () => {alive = false;ctrl.abort('component-unmount');};}, []);if (loading) return <div>加载中… {progress}%</div>;if (error) return <div>出错:{String(error)}</div>;return <pre>{JSON.stringify(data, null, 2)}</pre>;}

可抽象为自定义 Hook,便于复用:


// src/hooks/useConcurrentFetch.tsimport { useEffect, useState } from 'react';import {mapWithLimit,retry,fetchWithTimeout,} from '../utils/promise-all-utils.js';export function useConcurrentFetch(urls: string[],opts?: { limit?: number; timeout?: number; retries?: number }) {const { limit = 4, timeout = 8000, retries = 2 } = opts || {};const [data, setData] = useState<any[]>([]);const [loading, setLoading] = useState(true);const [error, setError] = useState<any>(null);useEffect(() => {let alive = true;const ctrl = new AbortController();(async () => {try {const out = await mapWithLimit(urls, limit, (u) =>retry(() =>fetchWithTimeout(u, timeout, { signal: ctrl.signal }).then((r) =>r.json()),{ retries }));if (!alive) return;setData(out);} catch (e) {if (!alive || e?.name === 'AbortError') return;setError(e);} finally {if (alive) setLoading(false);}})();return () => {alive = false;ctrl.abort();};}, [urls.join(','), limit, timeout, retries]);return { data, loading, error };}

提示:

  • 在 Next.js 等 SSR 环境中,组件端代码只在客户端运行这些工具;服务端可直接用 Promise.all/Node fetch,无需 AbortController(或按需 polyfill)。

  • 避免在 forEach 中使用 async,用 map + Promise.all 或本库的 mapWithLimit

Vue 3(Composition API)

setup 中使用 ref 管理状态,onMounted 触发加载,onBeforeUnmount 取消请求。


<!-- src/components/Dashboard.vue --><script setup>import { ref, onMounted, onBeforeUnmount } from 'vue';import {mapWithLimit,retry,fetchWithTimeout,progressAll,allProps,} from '../utils/promise-all-utils.js';const data = ref(null);const loading = ref(true);const error = ref(null);const progress = ref(0);let alive = true;const ctrl = new AbortController();onMounted(async () => {try {const base = await allProps({me: fetchWithTimeout('/api/me', 5000, { signal: ctrl.signal }).then((r) =>r.json()),settings: fetchWithTimeout('/api/settings', 5000, {signal: ctrl.signal,}).then((r) => r.json()),});const urls = ['/api/list/1', '/api/list/2', '/api/list/3'];const tasks = urls.map((u) => () =>retry(() =>fetchWithTimeout(u, 5000, { signal: ctrl.signal }).then((r) =>r.json()),{ retries: 3 }));const total = tasks.length;const lists = await progressAll(tasks.map((f) => f()),(d) => {if (alive) progress.value = Math.round((d / total) * 100);});if (!alive) return;data.value = { ...base, lists };} catch (e) {if (!alive || e?.name === 'AbortError') return;error.value = e;} finally {if (alive) loading.value = false;}});onBeforeUnmount(() => {alive = false;ctrl.abort('component-unmount');});</script><template><div><div v-if="loading">加载中… {{ progress }}%</div><div v-else-if="error">出错:{{ String(error) }}</div><pre v-else>{{ data }}</pre></div></template>

Vue 2(Options API)可在 created/beforeDestroy 生命周期中采用相同思路,使用 data 管理 loading/error/data,并在销毁时 controller.abort()

提示:

  • 在 Nuxt(SSR)中仅在客户端钩子中发起带 AbortController 的请求;服务器端可直接 await Promise.all

  • 大批量任务建议用 mapWithLimitbatch 分批,防止并发过高。

React Hook(useConcurrentFetch

/*
React Hook: useConcurrentFetch
- Features: concurrency limit, retry w/ backoff, per-request timeout+abort, progress, cancel, reload
- Sources can be:1) string | RequestInfo (fetch target)2) () => Promise<T>3) (signal?: AbortSignal) => Promise<T>
- Adjust the import path to your project structure as needed.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { mapWithLimit, retry, fetchWithTimeout } from './promise-all-utils.js';export type FetchSource<T> =| string| RequestInfo| (() => Promise<T>)| ((signal?: AbortSignal) => Promise<T>);export interface UseConcurrentFetchOptions<T> {limit?: number; // default 4timeout?: number; // ms, default 8000retries?: number; // default 2retryOptions?: {min?: number;factor?: number;max?: number;jitter?: boolean;shouldRetry?: (err: any, attempt: number) => boolean;onRetry?: (err: any, attempt: number, delayMs: number) => void;};mapper?: (response: Response, index: number) => Promise<T>; // for fetch sourcesimmediate?: boolean; // auto-run on mount, default true
}export interface UseConcurrentFetchState<T> {data: T[];loading: boolean;error: any;progress: number; // 0-100reload: () => void;cancel: () => void;
}export function useConcurrentFetch<T = any>(sources: FetchSource<T>[],options?: UseConcurrentFetchOptions<T>
): UseConcurrentFetchState<T> {const {limit = 4,timeout = 8000,retries = 2,retryOptions,mapper = (r: Response) => r.json() as Promise<T>,immediate = true,} = options || {};const [data, setData] = useState<T[]>([]);const [loading, setLoading] = useState<boolean>(!!immediate);const [error, setError] = useState<any>(null);const [progress, setProgress] = useState<number>(0);const versionRef = useRef<number>(0);const ctrlRef = useRef<AbortController | null>(null);const aliveRef = useRef<boolean>(true);useEffect(() => () => {aliveRef.current = false;ctrlRef.current?.abort('unmount');},[]);const key = useMemo(() => {// Best-effort stable key; consumers may pass memoized sources.try {return JSON.stringify(sources, (_, v) =>typeof v === 'function' ? 'fn' : v);} catch {return String(sources.length);}}, [sources]);const doLoad = useCallback(async (v: number) => {// Cancel any in-flightctrlRef.current?.abort('reload');const ctrl = new AbortController();ctrlRef.current = ctrl;setLoading(true);setError(null);setProgress(0);let done = 0;const total = sources.length || 0;try {const out = await mapWithLimit(sources, limit, async (src, idx) => {const runOnce = async () => {if (typeof src === 'function') {// Call with signal if accepted; functions can ignore extra arg safely in JSreturn (src as (signal?: AbortSignal) => Promise<T>)(ctrl.signal);} else {const res = await fetchWithTimeout(src as RequestInfo, timeout, {signal: ctrl.signal,});return mapper(res, idx);}};try {return await retry(runOnce, { retries, ...(retryOptions || {}) });} finally {done += 1;if (aliveRef.current && total > 0) {setProgress(Math.round((done / total) * 100));}}});if (!aliveRef.current || versionRef.current !== v) return;setData(out);setLoading(false);} catch (e: any) {if (e?.name === 'AbortError') return; // Silent on cancelif (!aliveRef.current || versionRef.current !== v) return;setError(e);setLoading(false);}},[sources, limit, timeout, retries, retryOptions, mapper]);const reload = useCallback(() => {versionRef.current += 1;const v = versionRef.current;void doLoad(v);}, [doLoad]);const cancel = useCallback(() => {ctrlRef.current?.abort('manual-cancel');}, []);useEffect(() => {if (immediate) reload();// eslint-disable-next-line react-hooks/exhaustive-deps}, [key, limit, timeout, retries, immediate]);return { data, loading, error, progress, reload, cancel };
}

vue

/*
Vue Composable: useConcurrentFetch
- Features: concurrency limit, retry w/ backoff, per-request timeout+abort, progress, cancel, reload
- Sources can be:1) string | RequestInfo (fetch target)2) () => Promise<T>3) (signal?: AbortSignal) => Promise<T>
- Accepts plain array or Ref of array for reactive sources
- Adjust the import path to your project structure as needed.
*/
import {ref,type Ref,computed,watch,onMounted,onBeforeUnmount,
} from 'vue';
import { mapWithLimit, retry, fetchWithTimeout } from './promise-all-utils.js';export type FetchSource<T> =| string| RequestInfo| (() => Promise<T>)| ((signal?: AbortSignal) => Promise<T>);export interface UseConcurrentFetchOptions<T> {limit?: number; // default 4timeout?: number; // ms, default 8000retries?: number; // default 2retryOptions?: {min?: number;factor?: number;max?: number;jitter?: boolean;shouldRetry?: (err: any, attempt: number) => boolean;onRetry?: (err: any, attempt: number, delayMs: number) => void;};mapper?: (response: Response, index: number) => Promise<T>; // for fetch sourcesimmediate?: boolean; // auto-run on mounted / when sources change, default true
}export interface UseConcurrentFetchResult<T> {data: Ref<T[]>;loading: Ref<boolean>;error: Ref<any>;progress: Ref<number>; // 0-100reload: () => void;cancel: () => void;
}export function useConcurrentFetch<T = any>(sources: FetchSource<T>[] | Ref<FetchSource<T>[]>,options?: UseConcurrentFetchOptions<T>
): UseConcurrentFetchResult<T> {const {limit = 4,timeout = 8000,retries = 2,retryOptions,mapper = (r: Response) => r.json() as Promise<T>,immediate = true,} = options || {};const data = ref<T[]>([]);const loading = ref<boolean>(!!immediate);const error = ref<any>(null);const progress = ref<number>(0);const alive = ref(true);let ctrl: AbortController | null = null;let version = 0;const sourcesRef: Ref<FetchSource<T>[]> = (Array.isArray(sources)? ref(sources as FetchSource<T>[]): (sources as Ref<FetchSource<T>[]>)) as Ref<FetchSource<T>[]>;const key = computed(() => {const s = sourcesRef.value;try {return JSON.stringify(s, (_, v) => (typeof v === 'function' ? 'fn' : v));} catch {return String(s.length);}});async function run(v: number) {// cancel in-flightctrl?.abort('reload');ctrl = new AbortController();loading.value = true;error.value = null;progress.value = 0;let done = 0;const list = sourcesRef.value.slice();const total = list.length || 0;try {const out = await mapWithLimit(list, limit, async (src, idx) => {const runOnce = async () => {if (typeof src === 'function') {return (src as (signal?: AbortSignal) => Promise<T>)(ctrl!.signal);} else {const res = await fetchWithTimeout(src as RequestInfo, timeout, {signal: ctrl!.signal,});return mapper(res, idx);}};try {return await retry(runOnce, { retries, ...(retryOptions || {}) });} finally {done += 1;if (alive.value && total > 0) {progress.value = Math.round((done / total) * 100);}}});if (!alive.value || version !== v) return;data.value = out;loading.value = false;} catch (e: any) {if (e?.name === 'AbortError') return;if (!alive.value || version !== v) return;error.value = e;loading.value = false;}}function reload() {version += 1;const v = version;void run(v);}function cancel() {ctrl?.abort('manual-cancel');}onMounted(() => {if (immediate) reload();});onBeforeUnmount(() => {alive.value = false;ctrl?.abort('component-unmount');});// react to sources changeswatch(() => [key.value, limit, timeout, retries, immediate],() => {if (immediate) reload();});return { data, loading, error, progress, reload, cancel };
}
http://www.dtcms.com/a/341420.html

相关文章:

  • 基于SpringBoot的蜗牛兼职网平台
  • React框架超详细入门到实战项目演练【前端】【React】
  • Spring Retry实战指南_让你的应用更具韧性
  • PyTorch API 2
  • 漫漫长夜 全DLC(The Long Dark)免安装中文版
  • Docker之MySQL安装
  • Redis(以Django为例,含具体操作步骤)
  • 数字人制作全流程解析:从概念到落地的完整步骤​
  • 实战:本地大模型+function Calling,获取北京天气
  • uniapp学习【上手篇】
  • [激光原理与应用-314]:光学设计 - 光学系统设计与电子电路设计的相似或相同点
  • 1-2前端撸码前的准备,包管理工具和环境搭建
  • 升级Android系统webview
  • Spring事务源码
  • Linux Capability 解析
  • 【81页PPT】智慧方案智能化弱电智能家居系统解决方案(附下载方式)
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘requests-html’问题
  • VPS服务器安全审计方案:从风险评估到防护实施
  • 汇编语言学习3---GDB调试
  • 【StarRocks】TabletChecker逻辑
  • 尝试给每个客户端设置一个标签身份,以此来解决非独立同分布的情况?
  • BM25 vs TF-IDF:经典文本检索方法的对比
  • 门控循环单元(GRU, Gated Recurrent Unit)
  • 压缩--RAR、7-Zip工具使用
  • 【Python】新手入门:python面向对象编程的三大特性是什么?python继承、封装、多态的特性都有哪些?
  • Jmeter接口测试
  • 30. 技术专题-锁
  • K8S-Configmap资源
  • 双模式 RTMP H.265 播放器解析:从国内扩展到 Enhanced RTMP 标准的演进
  • 媒体发稿平台哪家好?媒体新闻发稿渠道有哪些值得推荐?