核心问题:JavaScript 是单线程的。 想象 JS 引擎只有一个工人(主线程)。如果这个工人遇到一个需要很长时间才能完成的任务(比如网络请求、读取大文件),他要是停下来傻等,整个页面就卡住不动了,用户体验极差。为了解决这个问题,JS 引入了异步编程模型。事件循环、Promise、async/await 都是为了让这个单线程工人能高效地处理这些耗时任务而不卡顿。
1. 事件循环 (Event Loop):异步任务的"调度员"
- 它是啥? 事件循环是 JS 运行时(浏览器或 Node.js)内部的一个持续运行的机制 。它的职责就是监听 有没有任务要做,管理 任务队列,并决定什么时候把哪个任务交给 JS 主线程去执行。
- 核心工作流程 (简化版):
- 执行栈 (Call Stack): 这是主线程工作的地方,像个盘子,函数调用一层层叠起来(压栈),执行完就一个个移除(弹栈)。它一次只能执行一个任务(函数)。
- 任务队列 (Task Queues): 这里存放着等待执行的任务。关键点:队列不止一个!主要有两种:
- 宏任务队列 (MacroTask Queue): 放一些"大块头"的任务,常见的有:
setTimeout
,setInterval
,setImmediate
(Node.js),I/O 操作
(如网络请求完成、文件读写完成),UI rendering
(浏览器),事件回调
(如 click 事件)。 - 微任务队列 (MicroTask Queue): 放一些需要尽快 执行的小任务,优先级比宏任务高。常见的有:
Promise.then() / .catch() / .finally()
的回调,MutationObserver
(浏览器),process.nextTick
(Node.js - 优先级最高)。
- 宏任务队列 (MacroTask Queue): 放一些"大块头"的任务,常见的有:
- 循环过程:
- 主线程先执行当前执行栈中的所有同步任务(从上到下,一行行执行)。
- 当执行栈空了 (所有同步代码跑完),事件循环就开始工作:
- 它首先检查微任务队列。
- 如果微任务队列里有任务,一次性 把所有微任务按顺序拿出来,放到执行栈里执行干净!(清空微任务队列)
- 微任务队列清空后,事件循环会检查是否需要渲染(浏览器环境下),如果需要就执行 UI 渲染。
- 接着,事件循环从宏任务队列 里取出第一个任务(最老的),放到执行栈里执行。
- 执行完这个宏任务后,又回到第 2步:再次检查微任务队列,清空所有微任务 -> (可能渲染) -> 再取下一个宏任务...如此循环往复,这就是"事件循环"名字的由来。
- 为什么要有微任务? 为了给高优先级的回调(尤其是 Promise)提供插队的机会,确保异步操作的响应性更快。想象宏任务是排长队等公交,微任务就是救护车🚑,来了就得优先走。
2. Promise:异步操作的"承诺单"
-
它是啥? Promise 是一个对象 ,它代表一个异步操作的最终完成(或失败)及其结果值 。你可以把它想象成你去餐馆吃饭,服务员给你一张小票(Promise)。
- 这张小票承诺:你的菜最终会做好(成功)或者做不了(失败)。
- 在菜做好之前(异步操作进行中),你可以拿着小票干别的事(主线程继续执行其他代码)。
- 当菜好了(异步操作完成),服务员会根据小票找到你(调用你通过
.then()
或.catch()
注册的回调函数),把菜给你(传入结果)或者告诉你做不了(传入错误原因)。
-
三种状态 (不可逆):
pending
(进行中): 初始状态,菜还没开始做/正在做。fulfilled
(已成功): 操作成功完成,菜做好了!通过resolve(value)
触发。rejected
(已失败): 操作失败,菜做不了。通过reject(reason)
触发。
-
核心方法:
.then(onFulfilled, onRejected)
: 这是最核心的。它用来注册当 Promise 状态变为fulfilled
或rejected
时的回调函数。关键点:.then()
方法本身返回一个新的 Promise !这允许了链式调用 (promise.then(...).then(...).catch(...)
),是解决"回调地狱"的关键。onFulfilled
接收成功的结果 (value
)。onRejected
接收失败的原因 (reason
)。
.catch(onRejected)
: 专门用来处理错误的语法糖。它相当于.then(null, onRejected)
。通常放在链的末尾捕获前面任何步骤的错误。.finally(onFinally)
: 无论成功还是失败,最终都会执行的回调。适合做清理工作(比如关闭加载动画)。它不接收参数,也不知道最终结果是成功还是失败。
-
优点 (相比传统回调):
- 链式调用 (Chaining): 让异步流程更清晰、更线性,避免了层层嵌套的回调地狱 (
callback hell
)。 - 更好的错误处理: 通过
.catch
可以在链的末尾统一捕获错误,比在层层回调里分别处理err
参数更集中。 - 状态明确: 状态一旦改变就凝固,不可再变,逻辑更可靠。
- 链式调用 (Chaining): 让异步流程更清晰、更线性,避免了层层嵌套的回调地狱 (
-
例子 (点菜-做菜-上菜):
javascriptfunction orderDish(dishName) { // 1. 返回一个 Promise (相当于拿到小票) return new Promise((resolve, reject) => { console.log(`服务员:收到【${dishName}】订单,通知厨房...`); // 2. 模拟异步操作 (厨房做菜) setTimeout(() => { const isSuccess = Math.random() > 0.3; // 70% 成功几率 if (isSuccess) { console.log(`厨房:${dishName} 做好了!`); resolve(`热气腾腾的【${dishName}】`); // 3. 成功!把做好的菜传出去 } else { console.log(`厨房:抱歉,${dishName} 材料用完了!`); reject(`${dishName} 售罄了`); // 3. 失败!把原因传出去 } }, 2000); // 模拟做菜时间 2 秒 }); } // 顾客拿到小票 (Promise) 并注册回调 orderDish('鱼香肉丝') .then((dish) => { // 4. 成功回调:菜做好了 console.log(`顾客:收到 ${dish},开吃!`); return '一碗米饭'; // 5. 返回一个值 (会被包装成一个 fulfilled 状态的 Promise),继续点米饭 }) .then((sideDish) => { // 6. 接收上一个 then 返回的值 ('一碗米饭') console.log(`顾客:顺便要了 ${sideDish}`); }) .catch((error) => { // 7. 捕获链中任何地方的错误 console.log(`顾客:唉!${error}`); }) .finally(() => { console.log('顾客:用餐结束(无论成功失败都清理桌子)'); }); console.log('顾客:点完菜,先玩会儿手机...'); // 同步代码,立即执行
输出顺序:
服务员:收到【鱼香肉丝】订单,通知厨房...
(同步)顾客:点完菜,先玩会儿手机...
(同步)- (大约 2 秒后,成功时):
厨房:鱼香肉丝 做好了!
->顾客:收到 热气腾腾的【鱼香肉丝】,开吃!
->顾客:顺便要了 一碗米饭
->顾客:用餐结束(无论成功失败都清理桌子)
- (大约 2 秒后,失败时):
厨房:抱歉,鱼香肉丝 材料用完了!
->顾客:唉!鱼香肉丝 售罄了
->顾客:用餐结束(无论成功失败都清理桌子)
3. async/await:写异步代码像写同步代码的"语法糖"
-
它是啥?
async
和await
是 ES8 引入的关键字,它们是基于 Promise 的语法糖 。它们的目的是让异步代码的书写和阅读方式看起来和同步代码几乎一样,极大地提升了代码的可读性和可维护性。 -
async
函数:- 声明一个函数是异步的,只需在函数前面加
async
关键字:async function myAsyncFunc() { ... }
。 - 关键特性:
async
函数总是返回一个 Promise 对象!- 如果函数内部
return
一个值,这个值会被自动包装成一个fulfilled
状态的 Promise。 - 如果函数内部抛出错误 (
throw
),这个错误会被自动包装成一个rejected
状态的 Promise。
- 如果函数内部
- 声明一个函数是异步的,只需在函数前面加
-
await
表达式:- 只能在
async
函数内部使用。 await
后面通常跟一个 Promise 对象 (实际上,它可以跟任何值,如果是非 Promise,会直接返回该值)。await
的作用:暂停async
函数的执行 ,等待它后面的 Promise 状态稳定 (fulfilled
或rejected
)。- 如果等待的 Promise 变为
fulfilled
,await
表达式的值就是这个 Promise 的resolve
值。 - 如果等待的 Promise 变为
rejected
,await
表达式会抛出这个 Promise 的reject
原因 (就像throw error
一样),需要用try...catch
捕获。
- 只能在
-
优点:
- 代码极度清晰: 消除了
.then()
的链式调用,用看起来同步的顺序写异步逻辑。 - 错误处理更自然: 使用熟悉的
try...catch
结构来处理异步错误,和同步错误处理方式统一。 - 控制流更直观:
if/else
,for
,while
等控制语句可以自然地用在异步操作中。
- 代码极度清晰: 消除了
-
例子 (用 async/await 重写点菜):
javascriptasync function haveDinner() { try { console.log('顾客:开始点餐'); // 1. await 等待点菜(做菜)这个Promise完成,拿到结果 (做好的菜) const mainDish = await orderDish('鱼香肉丝'); // orderDish 返回 Promise console.log(`顾客:收到 ${mainDish},开吃!`); // 2. 点米饭 (这里没有异步依赖,但也可以用 await 接收返回值) const rice = '一碗米饭'; console.log(`顾客:顺便要了 ${rice}`); } catch (error) { // 3. 捕获 orderDish 或任何其他 await 表达式抛出的错误 console.log(`顾客:唉!${error}`); } finally { console.log('顾客:用餐结束(无论成功失败都清理桌子)'); } } haveDinner(); console.log('顾客:点完菜,先玩会儿手机...'); // 同步代码,立即执行
输出顺序: 和 Promise 链式调用的例子完全一致!但代码看起来就是从上到下的同步流程。
三者的关系与总结:
- 事件循环是地基: 它是 JS 运行时处理异步任务的根本机制。它决定了任务执行的顺序(先同步 -> 清空所有微任务 -> 执行一个宏任务 -> 再清空所有微任务 -> ...)。
- Promise 是标准化工具: 它提供了一种强大、标准化的方式来封装、组织和链式处理异步操作及其结果(成功值/失败原因)。它解决了回调地狱的核心问题,并定义了
.then
,.catch
,.finally
这些标准接口。 - async/await 是优雅外衣: 它们是基于 Promise 的语法糖。
async
声明异步函数并确保其返回 Promise。await
在async
函数内部暂停执行,等待一个 Promise 的完成,并直接获取其结果或捕获其错误。它让使用 Promise 的代码写起来像同步代码一样简单直观。 - 进化路线: 回调函数 (Callback Hell) -> Promise (链式调用解决嵌套) -> async/await (同步写法写异步,终极优雅方案)。
面试要点快速回顾:
- 事件循环: JS 单线程靠事件循环 + 任务队列(宏任务/微任务)实现非阻塞。执行栈空 -> 清空所有微任务 -> 执行一个宏任务 -> 再清空所有微任务 -> ... (微任务优先级高)。
- Promise: 表示异步操作最终状态(pending/fulfilled/rejected)。核心
.then(onFulfilled, onRejected)
注册回调并返回新 Promise 实现链式调用。.catch
处理错误,.finally
清理。 - async/await:
async
函数返回 Promise。await
暂停async
函数执行,等待 Promise 完成,返回结果值或抛出错误。用try...catch
处理错误。让异步代码像同步一样写。 - 关系: async/await 底层依赖 Promise,Promise 的执行顺序由事件循环调度。三者共同构成了 JS 现代异步编程的基石。