为什么需要异步
JavaScript 是单线程语言,同一个时刻只能做一件事。如果网络请求、文件读取都同步阻塞,页面就会卡死。异步编程的本质是”先不等待结果,把任务挂起,结果出来后通过回调/Promise 继续执行“。
一、从回调地狱到 Promise
回调时代最痛苦的是嵌套:
1 | // 回调地狱 |
Promise 把嵌套拍平:
1 | getUser(1) |
Promise 三种状态:pending(进行中)→ fulfilled(成功)或 rejected(失败),状态一旦改变不可逆。
二、Promise 常用方法
1 | // 全部成功才成功,一个失败整体失败 |
超时控制是 race 的经典场景:
1 | function timeout(ms) { |
三、async/await:异步的同步写法
1 | async function loadUserInfo() { |
注意点:
await只能在async函数里使用(顶层 await 需要 ESM 模块环境);async函数永远返回 Promise,返回值会被自动包装;- 用
try/catch捕获错误,和同步代码体验一致; - 没有依赖关系的请求不要串行 await,用
Promise.all并行:
1 | // 慢:两个请求串行,共耗时 t1 + t2 |
四、事件循环:微任务与宏任务
这是异步面试必考点。JavaScript 的异步任务分两类:
| 类型 | 例子 | 执行时机 |
|---|---|---|
| 宏任务(macrotask) | setTimeout、setInterval、I/O、requestAnimationFrame |
每轮事件循环取一个 |
| 微任务(microtask) | Promise.then、queueMicrotask、MutationObserver |
每个宏任务结束后清空全部微任务 |
执行顺序:同步代码 → 微任务 → 宏任务 → 微任务 → …
1 | console.log('1') // 同步 |
为什么微任务先于下一个宏任务? 因为微任务队列会在每个宏任务之间被完全清空。这也解释了为什么 setTimeout(() => {}, 0) 不一定在”0 毫秒”执行——它要等当前宏任务和所有微任务结束。
五、高频踩坑
- 在 forEach 里 await 不生效:
forEach不支持异步等待,要用for...of或Promise.all:
1 | // 错误:forEach 不会等待 await |
- 忘记 catch 导致未处理拒绝:
async函数里的异常不处理会变成 rejected Promise,Node 高版本会直接报 UnhandledPromiseRejection; - 滥用 await 拖慢速度:无依赖的请求务必并行;
- 回调里的 this 丢失:用箭头函数保持
this指向。
总结
异步编程的核心理解顺序:事件循环(微任务/宏任务)→ Promise 状态机 → async/await 语法糖 → 并行与错误处理。日常开发把”无依赖并行、有依赖串行、所有入口 try/catch”这三条记住,就能写出健壮的异步代码。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways!
评论
