JavaScript 异步编程模式

中等 🟡Js/Ts
12 个标签
预计阅读时间:98 分钟
JavaScript异步Promiseasync/await事件循环微任务宏任务生成器异步迭代器并发控制AbortController防抖节流

JavaScript 异步编程模式

JavaScript 是一门单线程语言,异步编程是其核心特性之一,掌握各种异步编程模式对于编写高性能的应用至关重要。本文将从最基础的回调函数出发,逐步深入到 Promise、async/await、事件循环、微任务与宏任务、生成器、异步迭代器、并发控制等主题,并结合大量真实案例、性能对比数据与常见踩坑,帮助你建立完整的异步编程知识体系。

为什么 JavaScript 需要异步

单线程的本质:

JavaScript 最初被设计为在浏览器中运行的脚本语言,用来操作 DOM、响应用户交互。如果 JavaScript 是多线程的,那么当一个线程修改 DOM 而另一个线程删除同一个 DOM 节点时,浏览器就无法确定应该以哪个操作为准。为了避免这种复杂的同步问题,JavaScript 被设计成单线程。

单线程带来的问题:

单线程意味着同一时刻只能做一件事。如果某个操作非常耗时(例如网络请求、读取大文件、复杂计算),那么在这个操作完成之前,整个线程都会被阻塞,页面无法响应用户的任何操作,表现为"卡死"。

类比:餐厅只有一个服务员

想象一家餐厅只有一位服务员(单线程)。如果这位服务员在为一桌客人点餐后,必须站在厨房门口一直等到这道菜做好、端上桌,才能去招待下一桌客人,那么效率会极其低下——后面的客人只能干等。

聪明的做法是:服务员记下这桌客人的订单(发起异步任务),把订单交给厨房(交给浏览器的其他线程/内核),然后立刻去招待下一桌客人。当某道菜做好时,厨房会通知服务员(回调/事件),服务员再把菜端过去。这样一位服务员就能同时"照顾"很多桌客人。这就是 JavaScript 异步编程的核心思想:遇到耗时操作时不要傻等,先去做别的事,等结果好了再回来处理

同步 vs 异步:

javascriptCode
// 同步代码:一行执行完才执行下一行
console.log('第一步');
const result = doHeavyCalculation(); // 假设耗时 3 秒,这 3 秒页面完全卡死
console.log('第二步:' + result);

// 异步代码:耗时操作交给后台,主线程继续往下走
console.log('第一步');
setTimeout(() => {
  console.log('第三步:延迟任务完成'); // 1 秒后才执行
}, 1000);
console.log('第二步'); // 立即执行,不会被上面的 setTimeout 阻塞

// 输出顺序:
// 第一步
// 第二步
// 第三步:延迟任务完成

异步的重要性:

保持界面流畅:耗时任务不阻塞主线程,用户交互始终有响应
提高吞吐:可以同时发起多个网络请求,充分利用等待时间
更好的资源利用:I/O 等待期间 CPU 可以处理其他任务

回调函数

回调函数的概念:

作为参数传递给其他函数的函数
在异步操作完成后被调用
是 JavaScript 中最早、最基础的异步处理方式

为什么回调重要:

回调是理解一切异步模式的基础。Promise、async/await 本质上都是对回调模式的封装与改进。理解回调的优点和缺陷,才能明白后续方案到底解决了什么问题。

回调的分类:

同步回调:立即执行,如数组的 forEach、map 传入的函数
异步回调:在未来某个时刻执行,如 setTimeout、事件监听、网络请求的回调

Node.js 的错误优先回调约定(error-first callback):

Node.js 生态约定回调的第一个参数永远是错误对象,如果没有错误则为 null,后续参数才是数据。这是为了统一错误处理。

回调地狱:

嵌套的回调函数层层堆叠,代码向右缩进越来越深
代码难以阅读和维护,俗称"回调地狱"或"末日金字塔"
错误处理复杂,每一层都要单独处理错误
无法用同步的 try/catch 统一捕获

代码示例

同步回调与异步回调

javascriptCode
// 同步回调:函数在当前调用栈内立即执行
[1, 2, 3].forEach(function (item) {
  console.log('同步回调:' + item);
});
console.log('forEach 之后');
// 输出顺序:同步回调 1 / 2 / 3 / forEach 之后

// 异步回调:函数被推迟到未来的事件循环执行
console.log('注册定时器之前');
setTimeout(function () {
  console.log('异步回调:定时器触发');
}, 0);
console.log('注册定时器之后');
// 输出顺序:注册定时器之前 / 注册定时器之后 / 异步回调

基本回调函数

javascriptCode
// 基本回调函数(错误优先约定)
function fetchData(callback) {
  setTimeout(() => {
    const data = { name: 'Alice', age: 25 };
    callback(null, data);
  }, 1000);
}

fetchData((error, data) => {
  if (error) {
    console.error('Error:', error);
    return;
  }
  console.log('Data:', data);
});

回调地狱

javascriptCode
function fetchData(cb) {
  setTimeout(() => cb(null, { id: 1, name: 'Alice' }), 1000);
}
function processData(data, cb) {
  setTimeout(() => cb(null, { ...data, processed: true }), 500);
}
function saveData(data, cb) {
  setTimeout(() => cb(null, { success: true, data }), 300);
}

// 回调地狱:层层嵌套,向右缩进越来越深
fetchData((error, data) => {
  if (error) {
    console.error('Error:', error);
    return;
  }

  processData(data, (error, processedData) => {
    if (error) {
      console.error('Error:', error);
      return;
    }

    saveData(processedData, (error, result) => {
      if (error) {
        console.error('Error:', error);
        return;
      }

      console.log('Result:', result);
    });
  });
});

回调地狱的真实场景

javascriptCode
// 真实业务:登录 -> 拉取用户资料 -> 拉取用户订单 -> 拉取订单详情
login(username, password, (err, token) => {
  if (err) return handleError(err);

  getUserProfile(token, (err, profile) => {
    if (err) return handleError(err);

    getUserOrders(profile.userId, (err, orders) => {
      if (err) return handleError(err);

      getOrderDetail(orders[0].id, (err, detail) => {
        if (err) return handleError(err);

        // 到这里已经缩进了五层,任何改动都很痛苦
        renderPage(profile, orders, detail);
      });
    });
  });
});

回调地狱的核心问题:

可读性差:业务逻辑被淹没在缩进和 if (err) 判断里
错误处理重复:每一层都要写一遍错误分支
难以复用:嵌套结构无法拆分成独立可组合的单元
控制流受限:想做并行、竞速、超时都非常繁琐

正是这些问题催生了 Promise。

Promise

Promise 的概念:

表示一个异步操作的最终完成或失败,以及它的结果值
是一个对象,代表"未来才会有结果的值"
有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
状态一旦从 pending 改变为 fulfilled 或 rejected 就不会再变,这个特性叫"状态凝固"

类比:取餐号

在快餐店点餐后,你会拿到一个取餐号(Promise 对象)。此时餐还没做好(pending),你不必站在柜台前干等,可以先去找座位。餐做好了会叫号(fulfilled),如果这道菜卖完了做不了也会通知你退款(rejected)。取餐号本身不是餐,而是"未来能拿到餐的凭证"。

Promise 的方法:

then():注册成功回调,处理成功的结果,返回一个新的 Promise
catch():注册失败回调,处理错误,等价于 then(null, onRejected)
finally():无论成功或失败都会执行,常用于清理工作(如关闭 loading)
Promise.resolve() / Promise.reject():快速创建一个已确定状态的 Promise
Promise.all():等待所有 Promise 完成,任意一个失败则整体失败
Promise.race():返回第一个确定状态(成功或失败)的 Promise 结果
Promise.allSettled():等待所有 Promise 完成或失败,返回每个的结果
Promise.any():返回第一个成功的 Promise,全部失败才失败

Promise 解决了什么:

链式调用替代嵌套,代码从"金字塔"变成"扁平流水线"
统一的错误冒泡机制,一个 catch 可以捕获链条上任意环节的错误
状态凝固保证结果稳定,不会被多次 resolve/reject 干扰
可组合:all / race / allSettled / any 让并发控制变得声明式

代码示例

Promise 基础示例

javascriptCode
// 创建 Promise
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = Math.random() > 0.5;

    if (success) {
      resolve({ name: 'Alice', age: 25 });
    } else {
      reject(new Error('Failed to fetch data'));
    }
  }, 1000);
});

// 使用 Promise
promise
  .then(data => {
    console.log('Data:', data);
    return data;
  })
  .catch(error => {
    console.error('Error:', error);
  })
  .finally(() => {
    console.log('Operation completed');
  });

Promise 的三种状态

javascriptCode
// pending -> fulfilled
const p1 = new Promise((resolve) => {
  setTimeout(() => resolve('成功值'), 500);
});

// pending -> rejected
const p2 = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('失败原因')), 500);
});

// 已经确定状态的 Promise,状态凝固后 resolve/reject 再调用无效
const p3 = new Promise((resolve, reject) => {
  resolve('第一次');
  resolve('第二次'); // 无效,被忽略
  reject(new Error('也无效')); // 无效,被忽略
});
p3.then(v => console.log(v)); // 输出:第一次

// 快捷创建
const immediate = Promise.resolve(42);
const failed = Promise.reject(new Error('立即失败'));
immediate.then(v => console.log(v)); // 42
failed.catch(e => console.log(e.message)); // 立即失败

Promise 链式调用

javascriptCode
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: 'Alice', age: 25 });
    }, 1000);
  });
}

function processData(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ ...data, processed: true });
    }, 500);
  });
}

function saveData(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ success: true, data });
    }, 300);
  });
}

// 链式调用:把回调地狱拉平成流水线
fetchData()
  .then(data => {
    console.log('Fetched:', data);
    return processData(data);
  })
  .then(data => {
    console.log('Processed:', data);
    return saveData(data);
  })
  .then(result => {
    console.log('Saved:', result);
  })
  .catch(error => {
    // 链条上任意环节出错都会走到这里
    console.error('Error:', error);
  });

then 中返回值的三种情况

javascriptCode
Promise.resolve(1)
  // 情况一:返回普通值,会被包装成 fulfilled 的 Promise
  .then(v => {
    console.log('第一步:', v); // 1
    return v + 1;
  })
  // 情况二:返回一个 Promise,会等它确定后再传递结果
  .then(v => {
    console.log('第二步:', v); // 2
    return new Promise(resolve => setTimeout(() => resolve(v * 10), 200));
  })
  // 情况三:抛出错误,会让返回的 Promise 变为 rejected
  .then(v => {
    console.log('第三步:', v); // 20
    throw new Error('故意抛错');
  })
  .then(v => {
    console.log('这一步不会执行:', v);
  })
  .catch(err => {
    console.log('捕获到错误:', err.message); // 故意抛错
  });

Promise.all 并发执行

javascriptCode
function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: `User ${id}` });
    }, 1000);
  });
}

function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, userId, title: 'Post 1' },
        { id: 2, userId, title: 'Post 2' }
      ]);
    }, 800);
  });
}

function fetchComments(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, postId, text: 'Comment 1' },
        { id: 2, postId, text: 'Comment 2' }
      ]);
    }, 600);
  });
}

// 并发执行:三个请求同时发出,总耗时约等于最慢的那个(1000ms)
Promise.all([
  fetchUser(1),
  fetchPosts(1),
  fetchComments(1)
])
  .then(([user, posts, comments]) => {
    console.log('User:', user);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  })
  .catch(error => {
    // 只要有一个失败,整体立即 reject
    console.error('Error:', error);
  });

Promise.all 的快速失败特性

javascriptCode
// Promise.all 是"快速失败"的:任意一个 reject,整体立即 reject
// 其余尚未完成的 Promise 并不会被取消,只是结果被忽略
const tasks = [
  Promise.resolve('任务 A 成功'),
  Promise.reject(new Error('任务 B 失败')),
  new Promise(resolve => setTimeout(() => resolve('任务 C 成功'), 1000))
];

Promise.all(tasks)
  .then(results => console.log('全部成功:', results))
  .catch(error => console.error('有失败:', error.message));
// 输出:有失败:任务 B 失败
// 注意:任务 C 仍会在 1 秒后完成,只是没人接收它的结果

Promise.race 超时处理

javascriptCode
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ data: 'Some data' });
    }, 2000);
  });
}

function timeout(ms) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('Timeout'));
    }, ms);
  });
}

// 使用 Promise.race 实现超时:谁先确定状态就用谁的结果
Promise.race([
  fetchData(),
  timeout(1000)
])
  .then(data => {
    console.log('Data:', data);
  })
  .catch(error => {
    // 数据请求要 2 秒,超时只要 1 秒,所以这里会捕获 Timeout
    console.error('Error:', error.message);
  });

Promise.allSettled 等待所有完成

javascriptCode
const promises = [
  Promise.resolve(1),
  Promise.reject(new Error('Failed')),
  Promise.resolve(3)
];

// allSettled 不会因为某个失败而中断,会等所有都确定状态
Promise.allSettled(promises)
  .then(results => {
    results.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Promise ${index}: ${result.value}`);
      } else {
        console.log(`Promise ${index}: ${result.reason.message}`);
      }
    });
  });
// 输出:
// Promise 0: 1
// Promise 1: Failed
// Promise 2: 3

Promise.any 返回第一个成功

javascriptCode
// any:只要有一个成功就成功,全部失败才失败(抛出 AggregateError)
const mirrors = [
  fetch('https://mirror1.example.com/file'),
  fetch('https://mirror2.example.com/file'),
  fetch('https://mirror3.example.com/file')
];

Promise.any(mirrors)
  .then(response => {
    console.log('最快返回的镜像:', response.url);
  })
  .catch(error => {
    // 所有镜像都失败时
    console.error('全部失败:', error.errors);
  });

Promise 组合方法行为对比表

| 方法 | 成功条件 | 失败条件 | 返回结果 | 典型用途 |

| --- | --- | --- | --- | --- |

| Promise.all | 全部成功 | 任意一个失败 | 成功值组成的数组 | 多个必须全部成功的并发请求 |

| Promise.allSettled | 总是"成功"(除非本身报错) | 从不 reject | 每项 status/value/reason 的数组 | 批量任务,需知道每个成败 |

| Promise.race | 第一个确定的是成功 | 第一个确定的是失败 | 第一个确定者的值或错误 | 超时控制、竞速 |

| Promise.any | 任意一个成功 | 全部失败 | 第一个成功值 | 多镜像取最快、容错请求 |

手写一个简易 Promise

javascriptCode
// 通过手写理解 Promise 的内部机制:状态机 + 回调队列
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class MyPromise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    // 状态还是 pending 时先把回调存起来,确定状态后再执行
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  then(onFulfilled, onRejected) {
    // 参数可选,做值穿透处理
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function'
      ? onRejected
      : err => { throw err; };

    // then 必须返回新的 Promise 才能链式调用
    const promise2 = new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        // 用 setTimeout 模拟微任务,保证异步执行
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        }, 0);
      };

      const handleRejected = () => {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolve(x);
          } catch (err) {
            reject(err);
          }
        }, 0);
      };

      if (this.status === FULFILLED) {
        handleFulfilled();
      } else if (this.status === REJECTED) {
        handleRejected();
      } else {
        // pending:先存入队列,等 resolve/reject 时触发
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });

    return promise2;
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  finally(callback) {
    return this.then(
      value => { callback(); return value; },
      reason => { callback(); throw reason; }
    );
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

// 测试
new MyPromise((resolve) => {
  setTimeout(() => resolve('手写 Promise 成功'), 300);
})
  .then(v => {
    console.log(v);
    return v.length;
  })
  .then(len => console.log('长度:', len));

手写 Promise.all

javascriptCode
function myAll(promises) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) {
      resolve(results);
      return;
    }

    promises.forEach((p, index) => {
      // 用 Promise.resolve 包装,兼容传入非 Promise 的值
      Promise.resolve(p).then(
        value => {
          results[index] = value; // 按原始顺序放置结果
          completed++;
          if (completed === total) {
            resolve(results);
          }
        },
        reason => reject(reason) // 任意失败立即整体失败
      );
    });
  });
}

myAll([
  Promise.resolve(1),
  new Promise(r => setTimeout(() => r(2), 100)),
  Promise.resolve(3)
]).then(res => console.log('myAll 结果:', res)); // [1, 2, 3]

手写 Promise.allSettled

javascriptCode
function myAllSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let completed = 0;
    const total = promises.length;

    if (total === 0) {
      resolve(results);
      return;
    }

    promises.forEach((p, index) => {
      Promise.resolve(p).then(
        value => {
          results[index] = { status: 'fulfilled', value };
          if (++completed === total) resolve(results);
        },
        reason => {
          results[index] = { status: 'rejected', reason };
          if (++completed === total) resolve(results);
        }
      );
    });
  });
}

事件循环与宏任务、微任务

事件循环(Event Loop)的概念:

事件循环是 JavaScript 运行时协调同步代码、异步回调、渲染的核心机制。它决定了"什么时候执行什么代码"。理解事件循环,才能准确预测异步代码的执行顺序,避免各种诡异的 bug。

核心组成:

调用栈(Call Stack):执行同步代码的地方,函数调用入栈、返回出栈
Web API / 宿主环境:浏览器或 Node 提供的能力(定时器、网络、DOM 事件),异步任务在这里等待
宏任务队列(Macrotask Queue / Task Queue):存放 setTimeout、setInterval、I/O、UI 事件等回调
微任务队列(Microtask Queue):存放 Promise 回调、queueMicrotask、MutationObserver 等
事件循环:不断检查调用栈是否为空,为空时按规则取任务执行

类比:餐厅点餐与叫号系统

再回到餐厅的比喻。服务员(主线程)先把手上正在做的事(同步代码,调用栈)做完。做完后,他会优先处理"插队的紧急小事"(微任务队列)——比如给刚上完菜的桌子递一下餐巾纸,这些琐事必须一次性全部处理干净。全部微任务处理完后,可能需要整理一下餐厅(渲染)。然后他才会去看"叫号大屏"(宏任务队列),叫下一位排队的客人(一个宏任务)。每处理完一个宏任务,又会把积攒的紧急小事(微任务)全部清空,如此循环往复。

关键规则(务必记住):

1.先执行完当前所有同步代码(清空调用栈)
2.然后清空整个微任务队列(执行过程中新产生的微任务也要在本轮清空)
3.视情况进行一次渲染
4.从宏任务队列取出一个宏任务执行
5.回到第 2 步,循环

一句话总结:每执行完一个宏任务,就把当前所有微任务清空。

为什么区分宏任务和微任务重要:

微任务优先级高于宏任务,Promise 回调总是先于 setTimeout 执行
微任务可以保证在本次事件循环末尾、下次渲染前执行,适合做状态更新收尾
不理解这个顺序,就无法解释很多面试题和实际现象

宏任务与微任务优先级表

| 类别 | 常见来源 | 执行时机 | 优先级 |

| --- | --- | --- | --- |

| 同步代码 | 普通函数调用 | 立即,占用调用栈 | 最高 |

| 微任务 | Promise.then/catch/finally、queueMicrotask、MutationObserver、await 之后的代码 | 当前宏任务结束后立即全部清空 | 高 |

| 宏任务 | setTimeout、setInterval、setImmediate(Node)、I/O、UI 事件、MessageChannel | 微任务清空后每轮取一个 | 低 |

| 渲染 | 浏览器重绘 | 微任务清空后、下一个宏任务前(约 16.6ms 一帧) | 介于两者之间 |

执行顺序推演示例一

javascriptCode
console.log('1 同步开始');

setTimeout(() => {
  console.log('2 宏任务 setTimeout');
}, 0);

Promise.resolve().then(() => {
  console.log('3 微任务 then');
});

console.log('4 同步结束');

// 推演过程:
// - 同步执行:打印 1,注册 setTimeout(进宏任务队列),注册 then(进微任务队列),打印 4
// - 调用栈清空,开始清微任务:打印 3
// - 微任务清空,取一个宏任务:打印 2
//
// 最终输出顺序:
// 1 同步开始
// 4 同步结束
// 3 微任务 then
// 2 宏任务 setTimeout

执行顺序推演示例二(嵌套)

javascriptCode
console.log('script start');

setTimeout(() => console.log('setTimeout'), 0);

Promise.resolve()
  .then(() => {
    console.log('promise1');
    // 在微任务里再注册微任务,本轮会继续清空
    Promise.resolve().then(() => console.log('promise1-nested'));
  })
  .then(() => {
    console.log('promise2');
  });

console.log('script end');

// 输出顺序:
// script start
// script end
// promise1
// promise1-nested   <- 注意:新产生的微任务在本轮就被清空
// promise2
// setTimeout        <- 宏任务最后执行

执行顺序推演示例三(async/await 混合)

javascriptCode
async function async1() {
  console.log('async1 start');
  await async2();
  // await 后面的代码相当于放进微任务队列
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => console.log('setTimeout'), 0);

async1();

new Promise((resolve) => {
  console.log('promise executor'); // executor 是同步执行的
  resolve();
}).then(() => {
  console.log('promise then');
});

console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise executor
// script end
// async1 end        <- await 之后的代码作为微任务
// promise then
// setTimeout

Node.js 与浏览器事件循环的差异

javascriptCode
// Node.js 特有的两个 API
// process.nextTick 优先级甚至高于 Promise 微任务
// setImmediate 属于 check 阶段的宏任务

setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));

console.log('同步代码');

// Node 中典型输出:
// 同步代码
// nextTick        <- nextTick 队列优先于微任务队列
// promise
// setTimeout / setImmediate 顺序在此场景下不确定,取决于进入事件循环的时机
//
// 浏览器没有 process.nextTick 和 setImmediate,
// 微任务统一由 Promise / queueMicrotask 提供

用 queueMicrotask 手动调度微任务

javascriptCode
console.log('开始');

queueMicrotask(() => {
  console.log('这是一个微任务');
});

Promise.resolve().then(() => console.log('Promise 微任务'));

console.log('结束');

// 输出:
// 开始
// 结束
// 这是一个微任务
// Promise 微任务

async/await

async/await 的概念:

基于 Promise 的语法糖,是目前最主流的异步写法
使异步代码看起来像同步代码,符合人类线性阅读习惯
更易于阅读和维护,可以用熟悉的 try/catch、for、if 等控制流

使用方法:

async 函数总是返回一个 Promise,即使返回普通值也会被自动包装
await 关键字只能在 async 函数中使用(顶层 await 需模块环境支持)
await 会"暂停"函数执行,直到右侧的 Promise 确定状态,再取出结果继续
await 一个非 Promise 值时,会把它当作已 resolve 的 Promise

async/await 相对 Promise 的优势:

顺序依赖的异步逻辑写起来像同步,没有 then 嵌套
错误处理回归 try/catch,符合直觉
在循环、条件分支中使用异步更自然

代码示例

async/await 基础示例

javascriptCode
async function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: 'Alice', age: 25 });
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log('Data:', data);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

async 函数一定返回 Promise

javascriptCode
async function returnValue() {
  return 42; // 返回普通值
}

// 即使返回普通值,调用结果也是 Promise
returnValue().then(v => console.log('拿到值:', v)); // 42

async function throwError() {
  throw new Error('出错了'); // 抛出错误
}

// 抛出的错误变成 rejected 的 Promise
throwError().catch(e => console.log('拿到错误:', e.message)); // 出错了

async/await 错误处理

javascriptCode
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

async function main() {
  try {
    const user = await fetchUser(1);
    console.log('User:', user);
  } catch (error) {
    console.error('Failed to fetch user:', error.message);
  }
}

main();

优雅的错误处理:await-to 模式

javascriptCode
// 借鉴 Go 的错误处理风格,避免大量 try/catch 嵌套
function to(promise) {
  return promise
    .then(data => [null, data])
    .catch(err => [err, null]);
}

async function main() {
  const [err1, user] = await to(fetchUser(1));
  if (err1) {
    console.error('获取用户失败:', err1.message);
    return;
  }

  const [err2, orders] = await to(fetchOrders(user.id));
  if (err2) {
    console.error('获取订单失败:', err2.message);
    return;
  }

  console.log('用户与订单:', user, orders);
}

async/await 并发执行

javascriptCode
async function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: `User ${id}` });
    }, 1000);
  });
}

async function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, userId, title: 'Post 1' },
        { id: 2, userId, title: 'Post 2' }
      ]);
    }, 800);
  });
}

async function fetchComments(postId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, postId, text: 'Comment 1' },
        { id: 2, postId, text: 'Comment 2' }
      ]);
    }, 600);
  });
}

// 并发执行:三个请求彼此无依赖,用 Promise.all 同时发起
async function main() {
  try {
    const [user, posts, comments] = await Promise.all([
      fetchUser(1),
      fetchPosts(1),
      fetchComments(1)
    ]);

    console.log('User:', user);
    console.log('Posts:', posts);
    console.log('Comments:', comments);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

async/await 顺序执行

javascriptCode
async function fetchUser(id) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id, name: `User ${id}` });
    }, 1000);
  });
}

async function fetchPosts(userId) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve([
        { id: 1, userId, title: 'Post 1' }
      ]);
    }, 800);
  });
}

// 顺序执行:后一步依赖前一步的结果时才用串行
async function main() {
  try {
    const user = await fetchUser(1);
    console.log('User:', user);

    const posts = await fetchPosts(user.id);
    console.log('Posts:', posts);
  } catch (error) {
    console.error('Error:', error);
  }
}

main();

常见误区:不必要的串行

javascriptCode
// 反例:两个请求彼此无依赖,却写成串行,白白多等待
async function badExample() {
  const user = await fetchUser(1);      // 等 1 秒
  const config = await fetchConfig();   // 再等 1 秒
  // 总耗时约 2 秒
  return { user, config };
}

// 正例:无依赖就并发,总耗时约等于较慢的那个
async function goodExample() {
  const [user, config] = await Promise.all([
    fetchUser(1),
    fetchConfig()
  ]);
  // 总耗时约 1 秒
  return { user, config };
}

在循环中使用 await

javascriptCode
// 场景一:需要严格顺序执行(例如后一次依赖前一次),用 for...of + await
async function processSequentially(items) {
  const results = [];
  for (const item of items) {
    const result = await processItem(item); // 一个接一个
    results.push(result);
  }
  return results;
}

// 场景二:彼此无依赖,希望并发,用 map + Promise.all
async function processConcurrently(items) {
  const results = await Promise.all(
    items.map(item => processItem(item))
  );
  return results;
}

// 陷阱:forEach 不会等待 await,回调是异步的但 forEach 本身不 await
async function wrongLoop(items) {
  items.forEach(async (item) => {
    await processItem(item); // forEach 不会等这些完成
  });
  console.log('这行会在所有 processItem 完成前就打印'); // 错误预期
}

async/await 的底层原理

核心结论:async/await 是"生成器(Generator)+ 自动执行器(协程)"的语法糖。

在 async/await 出现之前,社区就用生成器配合执行器(如 co 库)模拟出了类似的写法。async/await 相当于把这套机制内置到了语言层面。

理解链条:

生成器函数(function*)可以通过 yield 暂停和恢复执行,这就是"协程"的能力
await 类比 yield:都表示"在这里暂停,把控制权交出去"
async 函数体相当于一个生成器,await 相当于 yield 一个 Promise
自动执行器负责:拿到 yield/await 出来的 Promise,等它完成后,把结果再喂回生成器继续执行

协程的概念:

协程是一种可以主动让出执行权、之后再从让出点恢复的"轻量线程"。JavaScript 的生成器提供了协程的基础能力。async 函数就是运行在一个自动驱动的协程上:遇到 await 就挂起,Promise 完成后事件循环把它唤醒继续跑。

生成器(Generator)

生成器的概念:

用 function* 声明的特殊函数,调用后返回一个迭代器对象
函数体在遇到 yield 时暂停,并把 yield 后的值返回给调用者
通过迭代器的 next() 方法恢复执行,还可以把值传回函数内部
是实现异步流程控制、惰性序列、协程的基础

生成器基础

javascriptCode
function* simpleGenerator() {
  console.log('开始');
  yield 1; // 第一次 next 执行到这里暂停
  console.log('第一次恢复');
  yield 2; // 第二次 next 执行到这里暂停
  console.log('第二次恢复');
  return 3; // 第三次 next 结束
}

const gen = simpleGenerator();
console.log(gen.next()); // 打印"开始",返回 { value: 1, done: false }
console.log(gen.next()); // 打印"第一次恢复",返回 { value: 2, done: false }
console.log(gen.next()); // 打印"第二次恢复",返回 { value: 3, done: true }
console.log(gen.next()); // 返回 { value: undefined, done: true }

通过 next 向生成器传值

javascriptCode
function* dialogue() {
  const name = yield '你叫什么名字?';
  const age = yield `你好 ${name},你多大了?`;
  return `${name} 今年 ${age} 岁`;
}

const g = dialogue();
console.log(g.next().value);        // 你叫什么名字?
console.log(g.next('Alice').value); // 你好 Alice,你多大了?(把 Alice 传回给 name)
console.log(g.next(25).value);      // Alice 今年 25 岁(把 25 传回给 age)

用生成器 + 执行器模拟 async/await

javascriptCode
// 这段代码揭示了 async/await 的本质
function run(generatorFunc) {
  const gen = generatorFunc();

  function step(nextValue) {
    // 把上一步的结果喂回生成器,拿到下一个 yield 的 Promise
    const { value, done } = gen.next(nextValue);
    if (done) {
      return Promise.resolve(value);
    }
    // 等待 yield 出来的 Promise 完成,再驱动下一步
    return Promise.resolve(value).then(
      resolved => step(resolved),
      err => gen.throw(err) // 让错误在生成器内部的 try/catch 捕获
    );
  }

  return step();
}

function fetchNumber(n) {
  return new Promise(resolve => setTimeout(() => resolve(n * 2), 300));
}

// 用 yield 代替 await,效果几乎一样
run(function* () {
  const a = yield fetchNumber(1); // 相当于 await
  console.log('a =', a);          // 2
  const b = yield fetchNumber(a); // 相当于 await
  console.log('b =', b);          // 4
  return a + b;
}).then(total => console.log('total =', total)); // 6

// 对比等价的 async/await 写法:
// async function main() {
//   const a = await fetchNumber(1);
//   const b = await fetchNumber(a);
//   return a + b;
// }

生成器实现惰性无限序列

javascriptCode
// 生成器可以表示"无限"序列,因为它是惰性求值的
function* naturalNumbers() {
  let n = 1;
  while (true) {
    yield n++; // 每次 next 才生成下一个,不会真的死循环
  }
}

const nums = naturalNumbers();
console.log(nums.next().value); // 1
console.log(nums.next().value); // 2
console.log(nums.next().value); // 3

// 取前 n 个的工具
function take(iterator, count) {
  const result = [];
  for (let i = 0; i < count; i++) {
    result.push(iterator.next().value);
  }
  return result;
}
console.log(take(naturalNumbers(), 5)); // [1, 2, 3, 4, 5]

异步迭代器与 for await...of

异步迭代器的概念:

普通迭代器的 next() 返回 { value, done },异步迭代器的 next() 返回一个 Promise
用 Symbol.asyncIterator 定义,用 async function* 声明异步生成器
for await...of 用来遍历异步可迭代对象,会自动 await 每一项

为什么需要它:

当数据源是"一批一批异步到来"的(如分页接口、流式响应、逐块读取文件),异步迭代器提供了统一、优雅的消费方式,配合 for await...of 写起来像遍历普通数组一样自然。

异步生成器基础

javascriptCode
// async function* 声明异步生成器
async function* asyncCounter(max) {
  for (let i = 1; i <= max; i++) {
    // 模拟每次异步获取数据
    await new Promise(resolve => setTimeout(resolve, 200));
    yield i;
  }
}

async function main() {
  // for await...of 自动等待每一个 yield 的值
  for await (const num of asyncCounter(3)) {
    console.log('收到:', num); // 每隔 200ms 打印 1, 2, 3
  }
  console.log('遍历完成');
}

main();

用异步迭代器实现自动分页拉取

javascriptCode
// 真实场景:接口分页,每页返回 items 和是否有下一页
async function fetchPage(page) {
  const response = await fetch(`/api/list?page=${page}`);
  return response.json(); // { items: [...], hasNext: true/false }
}

// 把"分页接口"封装成一个异步可迭代对象
async function* paginate() {
  let page = 1;
  let hasNext = true;
  while (hasNext) {
    const data = await fetchPage(page);
    for (const item of data.items) {
      yield item; // 逐条产出
    }
    hasNext = data.hasNext;
    page++;
  }
}

async function main() {
  let count = 0;
  // 消费方完全不需要关心分页细节
  for await (const item of paginate()) {
    console.log('处理条目:', item.id);
    count++;
    if (count >= 100) break; // 可以随时中断,不会多拉数据
  }
  console.log('共处理', count, '条');
}

main();

自定义 Symbol.asyncIterator

javascriptCode
// 让任意对象支持 for await...of
const asyncRange = {
  from: 1,
  to: 5,
  [Symbol.asyncIterator]() {
    let current = this.from;
    const last = this.to;
    return {
      next() {
        return new Promise(resolve => {
          setTimeout(() => {
            if (current <= last) {
              resolve({ value: current++, done: false });
            } else {
              resolve({ value: undefined, done: true });
            }
          }, 100);
        });
      }
    };
  }
};

(async () => {
  for await (const value of asyncRange) {
    console.log(value); // 每隔 100ms 打印 1, 2, 3, 4, 5
  }
})();

AbortController 取消异步请求

为什么需要取消:

很多异步操作发起后,我们可能不再需要它的结果了:用户快速切换标签页、搜索框内容频繁变化、组件被卸载。如果不取消,不仅浪费带宽和计算,还可能导致"竞态条件"(旧请求的结果覆盖了新请求)。AbortController 提供了标准的取消机制。

核心 API:

new AbortController() 创建一个控制器
controller.signal 是一个信号对象,传给 fetch 等支持的 API
controller.abort() 触发取消,signal 会进入 aborted 状态,相关 Promise reject 一个 AbortError

基本用法:可取消的 fetch

javascriptCode
const controller = new AbortController();

fetch('/api/large-data', { signal: controller.signal })
  .then(response => response.json())
  .then(data => console.log('数据:', data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求被取消');
    } else {
      console.error('请求失败:', error);
    }
  });

// 3 秒后如果还没完成就取消
setTimeout(() => controller.abort(), 3000);

用 AbortController 实现请求超时

javascriptCode
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);

  return fetch(url, { ...options, signal: controller.signal })
    .finally(() => clearTimeout(timer)); // 完成后清理定时器
}

fetchWithTimeout('/api/data', {}, 3000)
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求超时被中断');
    }
  });

解决搜索竞态:取消上一次请求

javascriptCode
let currentController = null;

async function search(keyword) {
  // 有新请求时,先取消上一个还在进行的请求
  if (currentController) {
    currentController.abort();
  }
  currentController = new AbortController();

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(keyword)}`,
      { signal: currentController.signal }
    );
    const results = await response.json();
    renderResults(results); // 只有最新请求的结果会被渲染
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('搜索失败:', error);
    }
    // AbortError 说明是被新请求取消的,静默忽略即可
  }
}

并发控制与限制池

为什么要限制并发:

假设要发起 1000 个请求,如果一次性全部发出,会瞬间打满浏览器的连接数限制、拖垮服务器、甚至触发限流。合理的做法是维持一个"并发窗口",例如同时最多 5 个请求在跑,一个完成就补一个进来,直到全部完成。这就是并发限制池。

简单的并发控制(原始实现)

javascriptCode
async function fetchUrl(url) {
  const response = await fetch(url);
  return response.json();
}

async function fetchAll(urls, concurrency = 3) {
  const results = [];
  const executing = [];

  for (const url of urls) {
    const promise = fetchUrl(url).then(result => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });

    executing.push(promise);
    results.push(promise);

    if (executing.length >= concurrency) {
      await Promise.race(executing);
    }
  }

  return Promise.all(results);
}

const urls = [
  'https://api.example.com/data1',
  'https://api.example.com/data2',
  'https://api.example.com/data3',
  'https://api.example.com/data4',
  'https://api.example.com/data5'
];

fetchAll(urls, 3)
  .then(results => console.log('Results:', results))
  .catch(error => console.error('Error:', error));

更健壮的并发池(保持结果顺序)

javascriptCode
// 用"工人(worker)"模型实现并发池:开 n 个工人,每个工人不断从任务队列取任务
async function runWithConcurrency(tasks, concurrency = 5) {
  const results = new Array(tasks.length);
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < tasks.length) {
      const currentIndex = nextIndex++; // 领取一个任务下标
      try {
        results[currentIndex] = await tasks[currentIndex]();
      } catch (err) {
        results[currentIndex] = { error: err };
      }
    }
  }

  // 启动 concurrency 个工人并行消费
  const workers = [];
  for (let i = 0; i < concurrency; i++) {
    workers.push(worker());
  }
  await Promise.all(workers);

  return results; // 结果按任务原始顺序排列
}

// 使用:tasks 是"返回 Promise 的函数"数组(惰性,避免提前发起)
const ids = Array.from({ length: 20 }, (_, i) => i + 1);
const tasks = ids.map(id => () => fetch(`/api/item/${id}`).then(r => r.json()));

runWithConcurrency(tasks, 4).then(results => {
  console.log('全部完成,共', results.length, '项');
});

带并发上限的通用调度器类

javascriptCode
class TaskPool {
  constructor(concurrency = 5) {
    this.concurrency = concurrency;
    this.running = 0;
    this.queue = [];
  }

  // 加入一个返回 Promise 的任务函数,返回该任务的结果 Promise
  add(taskFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ taskFn, resolve, reject });
      this._schedule();
    });
  }

  _schedule() {
    // 只要还有空位且队列非空,就取任务执行
    while (this.running < this.concurrency && this.queue.length > 0) {
      const { taskFn, resolve, reject } = this.queue.shift();
      this.running++;
      Promise.resolve()
        .then(taskFn)
        .then(resolve, reject)
        .finally(() => {
          this.running--;
          this._schedule(); // 有任务完成,尝试补位
        });
    }
  }
}

// 使用
const pool = new TaskPool(3);
const jobs = Array.from({ length: 10 }, (_, i) => i);
Promise.all(
  jobs.map(i => pool.add(() =>
    new Promise(resolve => setTimeout(() => {
      console.log('完成任务', i);
      resolve(i);
    }, 500))
  ))
).then(() => console.log('全部任务完成'));

防抖与节流

防抖(debounce)和节流(throttle)是控制高频触发函数执行频率的两种经典技术,常用于搜索输入、滚动监听、窗口 resize、按钮防连点等场景。

防抖(debounce):

在事件被触发后 n 秒才执行回调,如果 n 秒内又被触发,则重新计时
类比:电梯门,只要不断有人进来就一直等,最后一个人进来 n 秒后才关门
适合:搜索框输入联想、表单输入校验、窗口 resize 结束后重新布局

节流(throttle):

在一段时间内,无论触发多少次,只执行一次回调
类比:水龙头限流,无论你怎么拧,单位时间只出这么多水
适合:滚动加载、鼠标移动、高频点击统计

防抖实现

javascriptCode
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    // 每次触发都清除上一次的定时器,重新计时
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}

// 使用:搜索框输入停止 300ms 后才真正发起请求
const onInput = debounce((event) => {
  console.log('发起搜索:', event.target.value);
}, 300);

// searchInput.addEventListener('input', onInput);

支持立即执行的防抖

javascriptCode
function debounce(fn, delay = 300, immediate = false) {
  let timer = null;
  return function (...args) {
    if (timer) clearTimeout(timer);

    if (immediate && !timer) {
      // 第一次立即执行,之后进入防抖
      fn.apply(this, args);
    }

    timer = setTimeout(() => {
      if (!immediate) {
        fn.apply(this, args);
      }
      timer = null;
    }, delay);
  };
}

节流实现(时间戳版)

javascriptCode
function throttle(fn, interval = 300) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    // 距离上次执行超过 interval 才执行
    if (now - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}

// 使用:滚动时最多每 200ms 处理一次
const onScroll = throttle(() => {
  console.log('处理滚动位置:', window.scrollY);
}, 200);

// window.addEventListener('scroll', onScroll);

节流实现(定时器版,保证最后一次执行)

javascriptCode
function throttle(fn, interval = 300) {
  let timer = null;
  return function (...args) {
    if (timer) return; // 定时器存在说明冷却期内,直接忽略
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, interval);
  };
}

防抖与节流对比

| 维度 | 防抖 debounce | 节流 throttle |

| --- | --- | --- |

| 核心思想 | 触发后等待,期间再触发就重新计时 | 固定时间窗口内只执行一次 |

| 执行时机 | 高频触发停止后执行一次 | 高频触发过程中周期性执行 |

| 类比 | 电梯门等最后一个人 | 水龙头限流匀速出水 |

| 适用场景 | 搜索联想、输入校验、resize 收尾 | 滚动加载、鼠标移动、按钮防连点 |

| 是否保证执行 | 停止触发后才执行 | 过程中稳定执行 |

真实案例一:电商详情页并发拉取

场景描述:

打开一个电商商品详情页,需要同时拉取:用户信息、购物车、商品详情、推荐商品、评价列表。这些接口彼此独立,如果串行请求会非常慢。我们对比串行与并行两种方案的耗时。

串行方案(反例)

javascriptCode
async function loadPageSerial(productId) {
  const start = Date.now();

  const user = await fetchUser();               // 200ms
  const cart = await fetchCart();               // 150ms
  const product = await fetchProduct(productId); // 300ms
  const recommends = await fetchRecommends(productId); // 250ms
  const reviews = await fetchReviews(productId); // 180ms

  console.log('串行总耗时:', Date.now() - start, 'ms');
  // 约 200+150+300+250+180 = 1080ms
  return { user, cart, product, recommends, reviews };
}

并行方案(正例)

javascriptCode
async function loadPageParallel(productId) {
  const start = Date.now();

  // 五个请求同时发出,总耗时约等于最慢的那个(300ms)
  const [user, cart, product, recommends, reviews] = await Promise.all([
    fetchUser(),
    fetchCart(),
    fetchProduct(productId),
    fetchRecommends(productId),
    fetchReviews(productId)
  ]);

  console.log('并行总耗时:', Date.now() - start, 'ms');
  // 约 300ms
  return { user, cart, product, recommends, reviews };
}

混合方案:部分依赖 + 部分并行

javascriptCode
// 实际业务往往是"部分请求依赖前置结果,其余可并行"
async function loadPageMixed(productId) {
  // 第一步:商品详情必须先拿到(后续推荐依赖它的分类)
  const product = await fetchProduct(productId);

  // 第二步:以下请求彼此独立,并行发起
  const [recommends, reviews, related] = await Promise.all([
    fetchRecommends(product.categoryId),
    fetchReviews(productId),
    fetchRelated(product.brandId)
  ]);

  return { product, recommends, reviews, related };
}

串行 vs 并行耗时对比表

| 方案 | 请求方式 | 各请求耗时 | 总耗时 | 相对串行提速 |

| --- | --- | --- | --- | --- |

| 串行 | 一个接一个 await | 200/150/300/250/180 | 约 1080ms | 基准 |

| 并行 | Promise.all 同时发起 | 同上(并发) | 约 300ms | 约 3.6 倍 |

| 混合 | 先拿商品再并行其余 | 300 + max(其余) | 约 550ms | 约 2 倍 |

结论:无依赖的请求一定要并行;有依赖时用混合方案,把能并行的部分尽量并行。

真实案例二:搜索框防抖 + 取消

场景描述:

用户在搜索框快速输入时,如果每次按键都发请求,会产生大量无效请求,还可能出现"竞态"——先发出的慢请求后返回,覆盖了后发出的快请求结果。解决方案是:防抖减少请求数量,AbortController 取消过期请求。

javascriptCode
function createSearch(renderResults) {
  let controller = null;

  // 内部真正的搜索逻辑
  async function doSearch(keyword) {
    if (!keyword.trim()) {
      renderResults([]);
      return;
    }

    // 取消上一个未完成的请求,避免竞态
    if (controller) controller.abort();
    controller = new AbortController();

    try {
      const res = await fetch(
        `/api/search?q=${encodeURIComponent(keyword)}`,
        { signal: controller.signal }
      );
      const data = await res.json();
      renderResults(data.results);
    } catch (err) {
      if (err.name !== 'AbortError') {
        console.error('搜索出错:', err);
      }
    }
  }

  // 用防抖包裹,输入停止 300ms 后才真正搜索
  const debouncedSearch = debounce(doSearch, 300);

  return function onInput(event) {
    debouncedSearch(event.target.value);
  };
}

// 使用
// const handler = createSearch(results => console.log('渲染:', results));
// searchInput.addEventListener('input', handler);

真实案例三:大列表分批请求(瀑布流)

场景描述:

需要为 1000 个商品逐个拉取库存信息,直接并发 1000 个请求会打垮服务。改用分批 + 并发控制:每批并发若干个,一批完成再进行下一批,并可展示进度。

分批处理(chunk 方式)

javascriptCode
// 把大数组切成固定大小的批次
function chunk(array, size) {
  const result = [];
  for (let i = 0; i < array.length; i += size) {
    result.push(array.slice(i, i + size));
  }
  return result;
}

async function fetchStockInBatches(productIds, batchSize = 10) {
  const batches = chunk(productIds, batchSize);
  const allResults = [];

  for (let i = 0; i < batches.length; i++) {
    const batch = batches[i];
    // 每批内部并发,批与批之间串行
    const batchResults = await Promise.all(
      batch.map(id => fetchStock(id))
    );
    allResults.push(...batchResults);

    console.log(`进度:${allResults.length}/${productIds.length}`);
  }

  return allResults;
}

const ids = Array.from({ length: 1000 }, (_, i) => i + 1);
fetchStockInBatches(ids, 10).then(stocks => {
  console.log('全部库存拉取完成,共', stocks.length, '项');
});

瀑布流滚动加载

javascriptCode
class InfiniteScroll {
  constructor({ container, loadMore, threshold = 200 }) {
    this.container = container;
    this.loadMore = loadMore;   // 返回 Promise 的加载函数
    this.threshold = threshold; // 距底部多少像素时触发
    this.loading = false;
    this.finished = false;
    this.page = 1;

    // 滚动监听用节流,避免高频触发
    this.onScroll = throttle(this._handleScroll.bind(this), 200);
    window.addEventListener('scroll', this.onScroll);
  }

  async _handleScroll() {
    if (this.loading || this.finished) return;

    const scrollBottom = window.innerHeight + window.scrollY;
    const pageHeight = document.body.offsetHeight;

    if (pageHeight - scrollBottom < this.threshold) {
      this.loading = true;
      try {
        const { items, hasMore } = await this.loadMore(this.page);
        this._render(items);
        this.page++;
        if (!hasMore) this.finished = true;
      } catch (err) {
        console.error('加载失败:', err);
      } finally {
        this.loading = false;
      }
    }
  }

  _render(items) {
    items.forEach(item => {
      const el = document.createElement('div');
      el.textContent = item.title;
      this.container.appendChild(el);
    });
  }

  destroy() {
    window.removeEventListener('scroll', this.onScroll);
  }
}

真实案例四:接口聚合与降级

场景描述:

首页需要聚合多个数据模块,但并非每个模块都是关键。用 Promise.allSettled 让部分模块失败时不影响整体,失败的模块降级为默认内容。

javascriptCode
async function loadHomePage() {
  const results = await Promise.allSettled([
    fetchBanners(),      // 轮播图
    fetchHotProducts(),  // 热销商品
    fetchNews(),         // 资讯
    fetchActivities()    // 活动
  ]);

  // 逐个处理,失败的降级为空数组或默认值
  const [banners, hotProducts, news, activities] = results.map((r, i) => {
    if (r.status === 'fulfilled') {
      return r.value;
    }
    console.warn(`模块 ${i} 加载失败,使用降级数据:`, r.reason.message);
    return []; // 降级
  });

  return { banners, hotProducts, news, activities };
}

请求重试与指数退避

为什么需要重试:

网络请求可能因为瞬时抖动、服务临时不可用而失败。对于幂等的读请求,自动重试几次往往就能成功。指数退避(每次重试间隔翻倍)可以避免在服务繁忙时反复冲击。

基础重试

javascriptCode
async function fetchWithRetry(url, options = {}, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, options);

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      return response.json();
    } catch (error) {
      if (i === retries - 1) {
        throw error; // 最后一次仍失败,抛出
      }

      // 指数退避:1s, 2s, 4s ...
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

fetchWithRetry('https://api.example.com/data')
  .then(data => console.log('Data:', data))
  .catch(error => console.error('Error:', error));

带抖动(jitter)的指数退避

javascriptCode
// 加入随机抖动,避免大量客户端在同一时刻集中重试(惊群效应)
async function fetchWithBackoff(url, options = {}, {
  retries = 5,
  baseDelay = 500,
  maxDelay = 10000
} = {}) {
  let attempt = 0;
  while (true) {
    try {
      const res = await fetch(url, options);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      attempt++;
      if (attempt > retries) throw err;

      // 指数增长并加入随机抖动,同时设上限
      const expDelay = Math.min(baseDelay * 2 ** (attempt - 1), maxDelay);
      const jitter = Math.random() * expDelay * 0.3;
      const delay = expDelay + jitter;
      console.log(`第 ${attempt} 次重试,等待 ${Math.round(delay)}ms`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}

其他实用异步工具

睡眠函数 sleep

javascriptCode
// 把 setTimeout 包装成可 await 的 sleep
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function demo() {
  console.log('开始');
  await sleep(1000);
  console.log('1 秒后');
  await sleep(500);
  console.log('再过 0.5 秒');
}

给任意 Promise 加超时

javascriptCode
function withTimeout(promise, ms, message = '操作超时') {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error(message)), ms);
  });
  return Promise.race([promise, timeout]);
}

// 使用
withTimeout(fetch('/api/slow').then(r => r.json()), 3000)
  .then(data => console.log(data))
  .catch(err => console.error(err.message)); // 超时会打印"操作超时"

简单的异步结果缓存(去重同一请求)

javascriptCode
function memoizeAsync(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) {
      return cache.get(key); // 命中缓存,直接返回同一个 Promise
    }
    const promise = fn(key).catch(err => {
      cache.delete(key); // 失败时清除缓存,允许下次重试
      throw err;
    });
    cache.set(key, promise);
    return promise;
  };
}

const getUser = memoizeAsync(id =>
  fetch(`/api/users/${id}`).then(r => r.json())
);

// 同一 id 的并发调用会复用同一个请求,避免重复发送
getUser(1);
getUser(1); // 不会再发一次请求

串行执行 Promise 数组(reduce 技巧)

javascriptCode
// 用 reduce 把一组任务串成链式,保证严格顺序执行
function runSequentially(taskFns) {
  return taskFns.reduce(
    (chain, taskFn) => chain.then(results =>
      taskFn().then(result => [...results, result])
    ),
    Promise.resolve([])
  );
}

const tasks = [
  () => sleep(300).then(() => 'A'),
  () => sleep(100).then(() => 'B'),
  () => sleep(200).then(() => 'C')
];

runSequentially(tasks).then(results => {
  console.log(results); // ['A', 'B', 'C'](严格按顺序)
});

常见坑与陷阱

坑一:忘记 await,拿到的是 Promise 而不是值

javascriptCode
async function bug1() {
  const user = fetchUser(1); // 忘了 await
  console.log(user.name);    // undefined,因为 user 是 Promise 对象
}

async function fixed1() {
  const user = await fetchUser(1);
  console.log(user.name);    // 正确
}

坑二:在 forEach 里用 await 不会等待

javascriptCode
// forEach 不感知 async 回调,不会等待
async function bug2(ids) {
  ids.forEach(async (id) => {
    await deleteItem(id);
  });
  console.log('删除完成'); // 会立即打印,此时删除可能都没开始完成
}

// 用 for...of 或 Promise.all 修复
async function fixed2(ids) {
  await Promise.all(ids.map(id => deleteItem(id))); // 并发
  console.log('删除完成'); // 真正完成后才打印
}

坑三:未处理的 Promise rejection

javascriptCode
// 没有 catch 的 Promise 抛错会产生 UnhandledPromiseRejection 警告
fetchUser(1).then(user => {
  throw new Error('处理出错'); // 没有 catch 接住
});

// 修复:总是加上 catch,或用 try/catch 包裹 await
fetchUser(1)
  .then(user => { throw new Error('处理出错'); })
  .catch(err => console.error('已捕获:', err.message));

坑四:Promise 构造器里再包一层没必要

javascriptCode
// 反例:多此一举地用 new Promise 包裹一个已有的 Promise
function bad() {
  return new Promise((resolve, reject) => {
    fetch('/api').then(resolve).catch(reject);
  });
}

// 正例:直接返回即可
function good() {
  return fetch('/api');
}

坑五:async 函数里的错误吞掉

javascriptCode
// 反例:catch 后什么都不做,错误被静默吞掉,难以排查
async function bad() {
  try {
    await risky();
  } catch (e) {
    // 空 catch,问题被隐藏
  }
}

// 正例:至少记录日志,或按业务重新抛出/降级
async function good() {
  try {
    await risky();
  } catch (e) {
    console.error('risky 失败:', e);
    throw e; // 或返回降级值
  }
}

坑六:串行的循环导致性能低下

javascriptCode
// 反例:明明可以并发,却在循环里逐个 await
async function bad(urls) {
  const results = [];
  for (const url of urls) {
    results.push(await fetch(url)); // 一个接一个,累加耗时
  }
  return results;
}

// 正例:并发(注意并发量,必要时配合并发池)
async function good(urls) {
  return Promise.all(urls.map(url => fetch(url)));
}

坑七:竞态条件导致旧结果覆盖新结果

javascriptCode
// 反例:快速切换时,慢的旧请求后返回,覆盖了新请求结果
let latestData;
async function bad(id) {
  const data = await fetchItem(id);
  latestData = data; // 无法保证这是最新的那次
}

// 正例:用请求序号或 AbortController 保证只采纳最新结果
let requestId = 0;
async function good(id) {
  const currentId = ++requestId;
  const data = await fetchItem(id);
  if (currentId === requestId) {
    latestData = data; // 只有最新一次才生效
  }
}

异步模式最佳实践

错误处理:

使用 try/catch 处理 async/await 错误
使用 catch() 处理 Promise 错误
统一错误处理策略,避免空 catch 吞错
区分可恢复错误(重试/降级)与致命错误(上报/中断)

并发控制:

无依赖的请求用 Promise.all 并行
批量任务用 allSettled 收集全部成败
需要竞速或超时用 race
多来源容错用 any
大批量请求用并发池限制并发数

性能优化:

避免不必要的串行 await
使用缓存减少重复请求(memoize、去重同一请求)
合理使用防抖和节流控制高频触发
及时用 AbortController 取消过期请求

代码组织:

优先使用 async/await 而非回调,提升可读性
把通用能力封装成工具(sleep、withTimeout、retry、pool)
保持函数职责单一,异步逻辑与业务逻辑分离

常见异步场景

网络请求:

使用 fetch 或 axios 发起请求
处理响应和错误
实现请求重试、超时与取消

文件操作:

读取和写入文件
处理大文件(流式读取,配合异步迭代器)
监控文件变化

定时器:

setTimeout 和 setInterval
清除定时器避免内存泄漏
使用 requestAnimationFrame 进行动画

事件处理:

处理 DOM 事件
事件委托
异步事件处理与竞态防护

总结

JavaScript 的异步编程经历了从回调函数,到 Promise,再到 async/await 的演进。每一次演进都在解决前一代方案的痛点:Promise 用链式调用和统一错误冒泡解决了回调地狱;async/await 用同步风格的语法让异步代码更易读易维护,其底层是生成器与协程。

理解事件循环、宏任务与微任务的执行顺序,是准确预测异步行为的基础。在实战中,要根据依赖关系选择串行或并行,用 Promise 的各种组合方法(all/allSettled/race/any)声明式地控制并发,用并发池限制资源占用,用防抖节流控制高频触发,用 AbortController 取消过期请求并防止竞态。

掌握这些模式与工具,配合完善的错误处理与性能优化意识,就能编写出既高性能又健壮、易维护的异步代码。

异步方案演进对比总结表

| 方案 | 出现时期 | 优点 | 缺点 | 适用场景 |

| --- | --- | --- | --- | --- |

| 回调函数 | 最早 | 简单直接,无需额外概念 | 回调地狱、错误处理繁琐、难组合 | 简单一次性异步、老代码 |

| Promise | ES2015 | 链式调用、统一错误冒泡、可组合 | then 链较长时仍显繁琐 | 需要并发组合、封装底层异步 |

| async/await | ES2017 | 同步风格、易读、易调试 | 需理解其非阻塞本质,易误用串行 | 绝大多数业务异步逻辑 |

| 生成器 + 执行器 | ES2015 | 可暂停恢复、协程能力 | 需手写执行器,已被 async 取代 | 学习原理、特殊流程控制 |

| 异步迭代器 | ES2018 | 优雅消费异步流/分页数据 | 概念较新,需 for await 支持 | 分页拉取、流式数据、逐块读取 |