为什么Promise.all()比顺序await调用更快?
异步请求调度与顺序await适用场景解析
1. JavaScript内部如何调度Promise.all的并行请求?
JavaScript基于事件循环机制处理异步任务,Promise.all的并行调度逻辑可拆解为以下步骤:
- 调用
fetch时,浏览器会立即发起HTTP请求(此步骤由浏览器网络线程处理,不属于JavaScript主线程),同时返回一个处于pending状态的Promise。 Promise.all会同时触发所有传入的Promise(即两个fetch请求),这些请求在浏览器网络层并行处理,互不阻塞。- JavaScript主线程继续执行同步任务,直到遇到
await关键字,此时async函数暂停执行,将控制权交还给事件循环。 - 当所有
fetch请求的响应返回后,对应的Promise变为resolved状态,结果被收集到微任务队列。 - 事件循环处理完当前宏任务队列的所有任务后,依次执行微任务队列中的任务,此时
await Promise.all(...)恢复执行,解构得到所有响应结果。
对比顺序await的差异:顺序执行时,必须等待第一个fetch的响应完全处理完成(包括解析JSON),才会发起第二个fetch请求,请求串行执行,总耗时为两个请求的时间之和。
2. 哪些场景下更适合使用顺序await调用?
以下场景优先选择顺序await而非并行请求:
- 请求存在依赖关系:后续请求需要使用前一个请求的返回数据(比如第二个请求的参数是第一个请求返回的ID),此时必须等待前序请求完成才能发起后续请求。
- 限流或避免服务器压力:当接口有请求频率限制,或者服务器资源有限时,串行请求可以避免短时间内发起大量请求导致的限流、报错或服务器负载过高。
- 按需中断流程:如果前一个请求失败时,后续请求没有执行的必要,顺序await可以在第一个请求抛出错误时直接终止函数执行,无需发起后续请求;而
Promise.all只要有一个Promise失败就会立即reject,但此时其他请求可能已经发起,无法取消。 - 资源加载依赖:某些场景下需要确保前一个资源加载完成后再加载依赖它的资源(比如先加载配置文件,再根据配置加载对应的业务数据)。
实现代码
版本1:顺序执行的getToDos(较慢版本)
/** * 版本1: getToDos(较慢版本) * 此处请求按顺序执行。 */ const getToDos = async () => { // 等待待办事项加载完成... const todoResponse = await fetch('https://jsonplaceholder.typicode.com/todos') const todos = await todoResponse.json() // ...之后才开始加载用户数据(耗时较长) const userResponse = await fetch('https://jsonplaceholder.typicode.com/users') const users = await userResponse.json() // 数据关联(合并) todos.forEach(t => { // 查找与待办事项userId匹配的用户 t.user = users.find(u => u.id == t.userId) }); return todos }
版本2:并行执行的getTodo2(高效版本)
/** * 版本2: getTodo2(高效版本) * 此处利用并行性节省时间。 */ const getTodo2 = async () => { // Promise.all同时启动两个请求。 // [todosRes, usersRes]是解构赋值——结果直接分配。 const [todosRes, usersRes] = await Promise.all([ fetch('https://jsonplaceholder.typicode.com/todos'), fetch('https://jsonplaceholder.typicode.com/users') ]); // 将响应数据转换为真实JavaScript格式(JSON) const todos = await todosRes.json(); const users = await usersRes.json(); // 数据合并:为每个待办事项分配其所属用户 todos.forEach(element => { element.user = users.find(user => user.id === element.userId); }); return todos; // 返回最终的"超级数组" };
展示函数:在网页上渲染数据
/** * 展示函数:在网页上显示数据 */ const showToDos = async () => { // 1. 获取数据(我们使用快速的版本2) const todos = await getTodo2(); // 2. 在HTML中找到"锚点"(<ul>元素) const todosElement = document.getElementById('todos'); // 3. 遍历每个待办事项并构建HTML todos.forEach(t => { // 创建一个新的列表元素(尚不可见) const li = document.createElement('li'); // 插入Bootstrap结构: // alert: 灰色边框 | form-check: 复选框样式 | badge: 名称标签 li.innerHTML = ` <div class="alert alert-light" role="alert"> <div class="form-check"> <input class="form-check-input" type="checkbox" ${t.completed ? 'checked' : ''} id="c-${t.id}"> <label class="form-check-label" for="c-${t.id}"> ${t.title} <span class="badge text-bg-secondary">${t.user.username}</span> </label> </div> </div>`; // 4. 添加交互性(复选框逻辑) // 在刚创建的li中查找复选框 const checkbox = li.querySelector("input"); // 事件监听器:响应复选框的勾选或取消 checkbox.addEventListener("change", () => { // 同步复选框状态与我们的数据对象(t) t.completed = checkbox.checked; // 调试:在控制台显示更新后的对象 console.log("状态已更改:", t); }); // 5. 将完成的li添加到网页的列表中 todosElement.appendChild(li); }); } // 启动:页面加载时执行该函数 showToDos();
HTML页面代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>To Do List</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <h1>To Do List</h1> <script src="app.js"></script> <ul id="todos"></ul> </body> </html>
内容的提问来源于stack exchange,提问作者Kerem Babo
相关产品推荐
相关产品推荐

