js如何在并发中同步两个fetch请求?

请求A的结果是B的参数,所以AB只能是同步请求,但有假设有100个AB请求,如何构造并发请求呢?


const get_cover = async (guid) => {
    // ...
    let response = await fetch(url)
    let data = await response.text()
    // ... url 从data中解析
    response = await fetch(url)
    data = await response.text()
    
    return data
}


const get_covers = async () => {
    const promises = comics.map(async comic => {
        try {
            // 图片地址
            comic.cover = await get_cover(comic.url);
        } catch (error) {
            console.error(`Error fetching cover from ${comic.url}:`, error);
        }
    });

    await Promise.all(promises);
}

这个代码完全不行,等待时间太长了。

假设一组AB耗时2s,上面的代码需要等待近1min才能获得数据,我希望能获得一组AB就显示一张图片,类似并行处理同步的AB?

谢谢!

阅读 10k
7 个回答
const pLimit = require('p-limit');
const limit = pLimit(20); // 这里设置并发限制为20,可以根据需要调整

const get_cover = async (guid) => {
    try {
        let response = await fetch(`https://example.com/api/a/${guid}`);
        let data = await response.text();
        let url = parseUrlFromData(data);
        response = await fetch(url);
        data = await response.text();
        return data;
    } catch (error) {
        console.error(`Error fetching cover for ${guid}:`, error);
        throw error;
    }
}

const get_covers_concurrently = async () => {
    const promises = comics.map(comic => limit(() => get_cover(comic.url).then(cover => {
        comic.cover = cover;
    }).catch(error => {
        console.error(`Error fetching cover from ${comic.url}:`, error);
    })));

    await Promise.allSettled(promises);
}

回答:浏览器是有最大请求数量限制的,一次最多同时发送6个请求,所以耗时久是自然的,你不可能让一个每次都需要2秒才能返回的接口在浏览器环境下去在一分钟内完成这些操作,考虑让后台去并发处理吧,然后采用websocket传到前台来,还有一个问题,图片资源短时间内加载这么多,带宽也挺不住啊,怎么会这么设计呢,这显然需要优化一下,你可以参考这个示例:https://gitee.com/anxwefndu/interface-performance-testing-tool

你用forEach不久行了?😅

comics.forEach(async (comic, index) => {
    try {
        // 图片地址
        comic.cover = await get_cover(comic.url);
        // 每拿到一个图片url就渲染,不用等其他的头像拿到了,再渲染到页面
        // 伪代码,不知道你实际渲染在页面上是用的哪个变量来渲染的
        // 写这个代码意思下
        this.renderDataList[index].cover = comic.cover
    } catch (error) {
        console.error(`Error fetching cover from ${comic.url}:`, error);
    }
})
const comics = [
    { url: 'comic1_url', title: 'Comic 1' },
    { url: 'comic2_url', title: 'Comic 2' },
    // ... 数据
];

const get_cover = async (url) => {
    try {
        // 请求A
        let response = await fetch(url);
        let data = await response.text();
        
        const url2 = `${url}/cover`; // 
        
        // 请求B - 获取封面
        response = await fetch(url2);
        data = await response.text();
        
        return data;
    } catch (error) {
        console.error(`Error in get_cover: ${error}`);
        throw error;
    }
}

const processResult = (comic, coverData) => {
    comic.cover = coverData;
    
    // 更新UI显示图片
    updateUI(comic);
}

// 更新UI
const updateUI = (comic) => {
    const container = document.getElementById('covers-container');
    const coverDiv = document.createElement('div');
    coverDiv.className = 'cover';
    coverDiv.innerHTML = `
        <h3>${comic.title}</h3>
        <img src="${comic.cover}" alt="${comic.title}">
    `;
    container.appendChild(coverDiv);
}

async function batchProcess(items, processFn, batchSize = 5) {
    const results = [];
    for (let i = 0; i < items.length; i += batchSize) {
        console.log(`Processing batch ${i/batchSize + 1}`);
        const batch = items.slice(i, i + batchSize);
        const batchPromises = batch.map(item => processFn(item));
        const batchResults = await Promise.allSettled(batchPromises);
        results.push(...batchResults);
    }
    return results;
}

const get_covers = async () => {
    // 加指示器
    const loadingIndicator = document.createElement('div');
    loadingIndicator.id = 'loading';
    loadingIndicator.textContent = 'Loading covers...';
    document.body.appendChild(loadingIndicator);

    try {
        await batchProcess(comics, async (comic) => {
            try {
                const coverData = await get_cover(comic.url);
                processResult(comic, coverData);
                return { comic, success: true };
            } catch (error) {
                console.error(`Failed to get cover for ${comic.url}:`, error);
                return { comic, success: false, error };
            }
        }, 5); // 同时处理5个请求
    } catch (error) {
        console.error('Error in get_covers:', error);
    } finally {
       
        document.getElementById('loading')?.remove();
    }
}

// 加样式
const style = document.createElement('style');
style.textContent = `
    #covers-container {
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 20px;
        padding: 20px;
    }
    
    .cover {
        border: 1px solid #ddd;
        padding: 10px;
        border-radius: 8px;
        text-align: center;
    }
    
    .cover img {
        max-width: 100%;
        height: auto;
    }
    
    #loading {
        position: fixed;
        top: 20px;
        right: 20px;
        background: #333;
        color: white;
        padding: 10px 20px;
        border-radius: 4px;
    }
`;
document.head.appendChild(style);

const container = document.createElement('div');
container.id = 'covers-container';
document.body.appendChild(container);

// 启动
get_covers();

或者手动控制:

const get_cover = async (guid) => {
    const responseA = await fetch(`https://api.example.com/endpointA/${guid}`);
    const dataA = await responseA.json();
    const urlB = dataA.coverUrl;
    
    const responseB = await fetch(urlB);
    const dataB = await responseB.json();
    
    return dataB.coverImage;
};

const get_covers = async (comics, concurrencyLimit = 10) => {
    let currentIndex = 0;
    const total = comics.length;
    const results = [];

    const worker = async () => {
        while (currentIndex < total) {
            const index = currentIndex++;
            const comic = comics[index];
            try {
                const cover = await get_cover(comic.guid);
                comic.cover = cover;
                
                // updateComicCover(comic.id, cover);
                console.log(`Fetched cover for comic ${comic.guid}`);
            } catch (error) {
                console.error(`Error fetching cover for comic ${comic.guid}:`, error);
            }
        }
    };

    const workers = [];
    for (let i = 0; i < concurrencyLimit; i++) {
        workers.push(worker());
    }

    await Promise.all(workers);
};

const comics = [
    { guid: 'comic1', url: 'https://example.com/comic1' },
    { guid: 'comic2', url: 'https://example.com/comic2' },
    // ... 100 个漫画对象
];

get_covers(comics, 10).then(() => {
    console.log('All covers fetched');
});

你拆成两个函数呗,一个就是循环n次发起请求,一个就只是按照A-B的执行顺序发起请求

下面是一段示例伪代码

// 模拟一个长度为100的数组集合
const list = Array.from({length: 100})

async function test() {
  console.time('test') // 开始计时
  // 同时发起所有的请求
  await Promise.all(list.map((item, index) => fetchFn(index)))
  console.timeEnd('test') // 计时结束
}
test()

// OP需求的自定义请求函数
async function fetchFn(index) {
  const res1 = await mockApi1(index)
  console.log('res1', res1)
  const res2 = await mockApi2(res1)
  console.log('res2', res2)
}

// 模拟请求1
function mockApi1(idx) {
  return new Promise((res) => {
    setTimeout(() => res(`mock-${idx}`), 300)
  })
}
// 模拟请求2
function mockApi2(prevRes) {
  return new Promise((res) => {
    setTimeout(() => res(`${prevRes}-next`), 300)
  })
}

// 输出结果:
// res1 mock-0
// res1 mock-1
// res1 mock-2
// res1 mock-3
// res1 mock-4
// res1 mock-5
// res1 mock-6
// res1 mock-7
// res1 mock-8
// res1 mock-9
// res1 mock-10
// res1 mock-11
// res1 mock-12
// res1 mock-13
// res1 mock-14
// res1 mock-15
// res1 mock-16
// res1 mock-17
// res1 mock-18
// res1 mock-19
// res1 mock-20
// res1 mock-21
// res1 mock-22
// res1 mock-23
// res1 mock-24
// res1 mock-25
// res1 mock-26
// res1 mock-27
// res1 mock-28
// res1 mock-29
// res1 mock-30
// res1 mock-31
// res1 mock-32
// res1 mock-33
// res1 mock-34
// res1 mock-35
// res1 mock-36
// res1 mock-37
// res1 mock-38
// res1 mock-39
// res1 mock-40
// res1 mock-41
// res1 mock-42
// res1 mock-43
// res1 mock-44
// res1 mock-45
// res1 mock-46
// res1 mock-47
// res1 mock-48
// res1 mock-49
// res1 mock-50
// res1 mock-51
// res1 mock-52
// res1 mock-53
// res1 mock-54
// res1 mock-55
// res1 mock-56
// res1 mock-57
// res1 mock-58
// res1 mock-59
// res1 mock-60
// res1 mock-61
// res1 mock-62
// res1 mock-63
// res1 mock-64
// res1 mock-65
// res1 mock-66
// res1 mock-67
// res1 mock-68
// res1 mock-69
// res1 mock-70
// res1 mock-71
// res1 mock-72
// res1 mock-73
// res1 mock-74
// res1 mock-75
// res1 mock-76
// res1 mock-77
// res2 mock-24-next
// res2 mock-25-next
// res2 mock-26-next
// res2 mock-27-next
// res2 mock-28-next
// res2 mock-29-next
// res2 mock-30-next
// res2 mock-31-next
// res2 mock-32-next
// res2 mock-33-next
// res2 mock-34-next
// res2 mock-35-next
// res2 mock-36-next
// res2 mock-37-next
// res2 mock-38-next
// res2 mock-39-next
// res2 mock-40-next
// res2 mock-41-next
// res2 mock-42-next
// res2 mock-43-next
// res2 mock-44-next
// res2 mock-45-next
// res2 mock-46-next
// res2 mock-47-next
// res2 mock-48-next
// res2 mock-49-next
// res2 mock-50-next
// res2 mock-51-next
// res2 mock-52-next
// res2 mock-53-next
// res2 mock-54-next
// res2 mock-55-next
// res2 mock-56-next
// res2 mock-57-next
// res2 mock-58-next
// res2 mock-59-next
// res2 mock-60-next
// res2 mock-61-next
// res2 mock-62-next
// res2 mock-63-next
// res2 mock-64-next
// res2 mock-65-next
// res2 mock-66-next
// res2 mock-67-next
// res2 mock-68-next
// res2 mock-69-next
// res2 mock-70-next
// res2 mock-71-next
// res2 mock-72-next
// res2 mock-73-next
// res2 mock-74-next
// res2 mock-75-next
// res2 mock-76-next
// res2 mock-77-next
// res2 mock-78-next
// res2 mock-79-next
// res2 mock-80-next
// res2 mock-81-next
// res2 mock-82-next
// res2 mock-83-next
// res2 mock-84-next
// res2 mock-85-next
// res2 mock-86-next
// res2 mock-87-next
// res2 mock-88-next
// res2 mock-89-next
// res2 mock-90-next
// res2 mock-91-next
// res2 mock-92-next
// res2 mock-93-next
// res2 mock-94-next
// res2 mock-95-next
// res2 mock-96-next
// res2 mock-97-next
// res2 mock-98-next
// res2 mock-99-next
// test:624 毫秒 - 倒计时结束

可以考虑Promise.all

const res = await Promise.all([
    fetch('/a').then(res=>res.json()),
    fetch('/b').then(res=>res.text()),
])

console.log(res) // [res1,res2]
新手上路,请多包涵

如果想获得一组AB就显示一张图片,就不要使用Promise.all

const get_covers = async () => {
    const promises = comics.map(async (comic,index) => {
        try {
            // imgGroup是你要展示图片的img节点的数组
            imgGroup[index].src = await get_cover(comic.url);
        } catch (error) {
            console.error(`Error fetching cover from ${comic.url}:`, error);
        }
    });
}
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏