一、前置认知:JS 单线程模型
JavaScript 是单线程语言,同一时间只能做一件事。 单线程避免了多线程资源抢占问题,但如果遇到耗时操作(网络请求、定时器、文件读取),会阻塞后面代码执行。 异步方案就是用来解决这个矛盾:耗时任务交给浏览器内核其他线程处理,主线程继续往下执行,任务完成后再回调执行结果。
核心概念:事件循环 Event Loop
- 同步代码优先执行,直接进入调用栈
- 异步任务分为:宏任务、微任务
- 执行顺序:同步代码 → 清空所有微任务队列 → 取一个宏任务执行,循环往复
- 微任务:
Promise.then/catch/finally、queueMicrotask - 宏任务:
setTimeout、setInterval、AJAX、DOM 渲染
二、回调函数 Callback
基础定义
把一个函数作为参数传给另一个函数,等异步操作完成之后再执行这个传入函数,就是回调。
// 简单定时器回调 console.log('开始') setTimeout(function () { console.log('定时器回调执行') }, 1000) console.log('结束')输出顺序:
开始 结束 定时器回调执行回调地狱(经典痛点)
多层异步依赖,回调层层嵌套,代码向右无限缩进,可读性、可维护性极差:
// 回调地狱示例 ajax('/api/user', function(res1) { ajax(`/api/order?uid=${res1.id}`, function(res2) { ajax(`/api/goods?oid=${res2.id}`, function(res3) { console.log('最终数据', res3) }, err3 => {}) }, err2 => {}) }, err1 => {})✅ 优点:简单、原生支持,早期唯一异步方案 ❌ 缺点:嵌套臃肿、错误处理分散、无法用 return /try catch、难以串行控制
三、Promise
Promise 是 ES6 推出的异步容器,用来包装异步操作,解决回调地狱。
三种状态(状态不可逆)
pending:进行中(初始状态)fulfilled:已成功rejected:已失败
基础语法
const p = new Promise(function(resolve, reject) { // 异步操作 setTimeout(() => { const ok = true if (ok) { resolve('成功结果') // 触发 .then } else { reject('失败原因') // 触发 .catch } }, 1000) }) p.then(res => { console.log('拿到结果:', res) }).catch(err => { console.error('错误:', err) }).finally(() => { console.log('无论成功失败都会执行') })链式调用(核心优势)
.then内部 return 新 Promise,实现串行异步,不再嵌套:
new Promise(resolve => resolve(1)) .then(res => { console.log(res) return 2 }) .then(res => { console.log(res) return new Promise(resolve => resolve(3)) }) .then(res => console.log(res))Promise 静态方法
表格
| API | 作用 |
|---|---|
Promise.all([p1,p2,p3]) | 全部成功才成功,任意一个失败直接 reject |
Promise.race([p1,p2,p3]) | 谁先有结果(成功 / 失败)就返回谁 |
Promise.allSettled([p1,p2,p3]) | 等待全部完成,不管成功失败,返回每个结果状态 |
Promise.any([p1,p2,p3]) | 任意一个成功就成功;全部失败才 reject |
示例:
// all 并发请求 const p1 = fetch('/api/a') const p2 = fetch('/api/b') Promise.all([p1, p2]).then(([res1, res2]) => { // 两个接口都拿到数据 })✅ 优点:链式写法扁平、统一错误捕获、支持并发控制 ❌ 缺点:依旧是回调风格,不能直观像同步代码一样书写;无法中途取消
四、async /await
ES2017 语法糖,基于 Promise 封装,让异步代码看起来像同步代码,是目前项目首选方案。
基础规则
async修饰函数,函数返回值自动包装成 Promiseawait只能写在async函数内部await后面一般跟 Promise,会暂停当前代码,直到 Promise 完成,拿到结果
// 基础示例 async function getData() { try { const res1 = await new Promise(resolve => setTimeout(() => resolve('数据1'), 1000)) console.log(res1) const res2 = await new Promise(resolve => setTimeout(() => resolve('数据2'), 1000)) console.log(res2) } catch (err) { console.error('捕获异步错误', err) } } getData()并发场景搭配 Promise.all
await 直接串行等待,如果不需要依赖,不要依次 await,避免浪费时间:
async function batch() { // 同时发起 const p1 = fetch('/api/a') const p2 = fetch('/api/b') const [r1, r2] = await Promise.all([p1, p2]) }✅ 优点:代码直观、可读性最强,错误统一 try/catch ❌ 缺点:await 会阻塞当前 async 函数内部代码;必须依托 Promise
五、方案选型对照表
表格
| 方案 | 适用场景 | 推荐度 |
|---|---|---|
| 回调函数 | 简单一次性异步、老旧兼容代码 | ⭐ |
| Promise 链式 | 简单异步串行、需要并发控制 | ⭐⭐⭐ |
| async + await | 业务接口请求、复杂异步流程(日常开发首选) | ⭐⭐⭐⭐⭐ |
六、高频踩坑清单
await只能在 async 函数里,普通函数直接使用会语法报错- 忘记 try-catch:await 的 Promise reject 会抛出未捕获异常
- 无依赖接口串行 await:白白拉长总耗时,优先 Promise.all
- 混淆微任务宏任务:Promise.then 比 setTimeout 先执行
- Promise 状态一旦改变,无法二次修改
七、拓展:异步迭代器 & Generator(了解)
Generator(function*+ yield)早期用来实现异步流程控制,现在项目里很少用,被 async/await 替代,框架底层源码偶尔可见。
小结
异步是 JS 核心难点,演进路线:回调函数 → Promise → async/await。现代前端业务开发,优先选择 async/await + Promise 组合。