JavaScript 中 事件循环,Promise和async/await的详解

核心问题:JavaScript 是单线程的。 想象 JS 引擎只有一个工人(主线程)。如果这个工人遇到一个需要很长时间才能完成的任务(比如网络请求、读取大文件),他要是停下来傻等,整个页面就卡住不动了,用户体验极差。为了解决这个问题,JS 引入了异步编程模型。事件循环、Promise、async/await 都是为了让这个单线程工人能高效地处理这些耗时任务而不卡顿。

1. 事件循环 (Event Loop):异步任务的"调度员"

  • 它是啥? 事件循环是 JS 运行时(浏览器或 Node.js)内部的一个持续运行的机制 。它的职责就是监听 有没有任务要做,管理 任务队列,并决定什么时候把哪个任务交给 JS 主线程去执行。
  • 核心工作流程 (简化版):
    1. 执行栈 (Call Stack): 这是主线程工作的地方,像个盘子,函数调用一层层叠起来(压栈),执行完就一个个移除(弹栈)。它一次只能执行一个任务(函数)。
    2. 任务队列 (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 - 优先级最高)。
    3. 循环过程:
      • 主线程先执行当前执行栈中的所有同步任务(从上到下,一行行执行)。
      • 当执行栈空了 (所有同步代码跑完),事件循环就开始工作:
        • 它首先检查微任务队列
        • 如果微任务队列里有任务,一次性所有微任务按顺序拿出来,放到执行栈里执行干净!(清空微任务队列)
        • 微任务队列清空后,事件循环会检查是否需要渲染(浏览器环境下),如果需要就执行 UI 渲染。
        • 接着,事件循环从宏任务队列 里取出第一个任务(最老的),放到执行栈里执行。
        • 执行完这个宏任务后,又回到第 2步:再次检查微任务队列,清空所有微任务 -> (可能渲染) -> 再取下一个宏任务...如此循环往复,这就是"事件循环"名字的由来。
  • 为什么要有微任务? 为了给高优先级的回调(尤其是 Promise)提供插队的机会,确保异步操作的响应性更快。想象宏任务是排长队等公交,微任务就是救护车🚑,来了就得优先走。

2. Promise:异步操作的"承诺单"

  • 它是啥? Promise 是一个对象 ,它代表一个异步操作的最终完成(或失败)及其结果值 。你可以把它想象成你去餐馆吃饭,服务员给你一张小票(Promise)

    • 这张小票承诺:你的菜最终会做好(成功)或者做不了(失败)。
    • 在菜做好之前(异步操作进行中),你可以拿着小票干别的事(主线程继续执行其他代码)。
    • 当菜好了(异步操作完成),服务员会根据小票找到你(调用你通过 .then().catch() 注册的回调函数),把菜给你(传入结果)或者告诉你做不了(传入错误原因)。
  • 三种状态 (不可逆):

    • pending (进行中): 初始状态,菜还没开始做/正在做。
    • fulfilled (已成功): 操作成功完成,菜做好了!通过 resolve(value) 触发。
    • rejected (已失败): 操作失败,菜做不了。通过 reject(reason) 触发。
  • 核心方法:

    • .then(onFulfilled, onRejected) 这是最核心的。它用来注册当 Promise 状态变为 fulfilledrejected 时的回调函数。关键点:
      • .then() 方法本身返回一个新的 Promise !这允许了链式调用 (promise.then(...).then(...).catch(...)),是解决"回调地狱"的关键。
      • onFulfilled 接收成功的结果 (value)。
      • onRejected 接收失败的原因 (reason)。
    • .catch(onRejected) 专门用来处理错误的语法糖。它相当于 .then(null, onRejected)。通常放在链的末尾捕获前面任何步骤的错误。
    • .finally(onFinally) 无论成功还是失败,最终都会执行的回调。适合做清理工作(比如关闭加载动画)。它不接收参数,也不知道最终结果是成功还是失败。
  • 优点 (相比传统回调):

    • 链式调用 (Chaining): 让异步流程更清晰、更线性,避免了层层嵌套的回调地狱 (callback hell)。
    • 更好的错误处理: 通过 .catch 可以在链的末尾统一捕获错误,比在层层回调里分别处理 err 参数更集中。
    • 状态明确: 状态一旦改变就凝固,不可再变,逻辑更可靠。
  • 例子 (点菜-做菜-上菜):

    javascript 复制代码
    function 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('顾客:点完菜,先玩会儿手机...'); // 同步代码,立即执行

    输出顺序:

    1. 服务员:收到【鱼香肉丝】订单,通知厨房... (同步)
    2. 顾客:点完菜,先玩会儿手机... (同步)
    3. (大约 2 秒后,成功时): 厨房:鱼香肉丝 做好了! -> 顾客:收到 热气腾腾的【鱼香肉丝】,开吃! -> 顾客:顺便要了 一碗米饭 -> 顾客:用餐结束(无论成功失败都清理桌子)
    4. (大约 2 秒后,失败时): 厨房:抱歉,鱼香肉丝 材料用完了! -> 顾客:唉!鱼香肉丝 售罄了 -> 顾客:用餐结束(无论成功失败都清理桌子)

3. async/await:写异步代码像写同步代码的"语法糖"

  • 它是啥? asyncawait 是 ES8 引入的关键字,它们是基于 Promise 的语法糖 。它们的目的是让异步代码的书写和阅读方式看起来和同步代码几乎一样,极大地提升了代码的可读性和可维护性。

  • async 函数:

    • 声明一个函数是异步的,只需在函数前面加 async 关键字:async function myAsyncFunc() { ... }
    • 关键特性:async 函数总是返回一个 Promise 对象!
      • 如果函数内部 return 一个值,这个值会被自动包装成一个 fulfilled 状态的 Promise
      • 如果函数内部抛出错误 (throw),这个错误会被自动包装成一个 rejected 状态的 Promise
  • await 表达式:

    • 只能在 async 函数内部使用。
    • await 后面通常跟一个 Promise 对象 (实际上,它可以跟任何值,如果是非 Promise,会直接返回该值)。
    • await 的作用:暂停 async 函数的执行 ,等待它后面的 Promise 状态稳定 (fulfilledrejected)。
    • 如果等待的 Promise 变为 fulfilledawait 表达式的值就是这个 Promise 的 resolve 值。
    • 如果等待的 Promise 变为 rejectedawait 表达式会抛出这个 Promise 的 reject 原因 (就像 throw error 一样),需要用 try...catch 捕获。
  • 优点:

    • 代码极度清晰: 消除了 .then() 的链式调用,用看起来同步的顺序写异步逻辑。
    • 错误处理更自然: 使用熟悉的 try...catch 结构来处理异步错误,和同步错误处理方式统一。
    • 控制流更直观: if/else, for, while 等控制语句可以自然地用在异步操作中。
  • 例子 (用 async/await 重写点菜):

    javascript 复制代码
    async 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 链式调用的例子完全一致!但代码看起来就是从上到下的同步流程。

三者的关系与总结:

  1. 事件循环是地基: 它是 JS 运行时处理异步任务的根本机制。它决定了任务执行的顺序(先同步 -> 清空所有微任务 -> 执行一个宏任务 -> 再清空所有微任务 -> ...)。
  2. Promise 是标准化工具: 它提供了一种强大、标准化的方式来封装、组织和链式处理异步操作及其结果(成功值/失败原因)。它解决了回调地狱的核心问题,并定义了 .then, .catch, .finally 这些标准接口。
  3. async/await 是优雅外衣: 它们是基于 Promise 的语法糖。async 声明异步函数并确保其返回 Promise。awaitasync 函数内部暂停执行,等待一个 Promise 的完成,并直接获取其结果或捕获其错误。它让使用 Promise 的代码写起来像同步代码一样简单直观。
  4. 进化路线: 回调函数 (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 现代异步编程的基石。
相关推荐
萌萌哒草头将军9 分钟前
🚀🚀🚀尤雨溪:Vite 和 JavaScript 工具的未来
前端·vue.js·vuex
Fly-ping17 分钟前
【前端】cookie和web stroage(localStorage,sessionStorage)的使用方法及区别
前端
我家媳妇儿萌哒哒1 小时前
el-upload 点击上传按钮前先判断条件满足再弹选择文件框
前端·javascript·vue.js
天天向上10241 小时前
el-tree按照用户勾选的顺序记录节点
前端·javascript·vue.js
sha虫剂1 小时前
如何用div手写一个富文本编辑器(contenteditable=“true“)
前端·vue.js·typescript
咔咔库奇1 小时前
深入探索 Vue 3 Fragments:从原理到实战的全方位指南
前端·javascript·vue.js
要加油哦~1 小时前
vue | vue 插件化机制,全局注册 和 局部注册
前端·javascript·vue.js
猫头虎-前端技术1 小时前
HTML 与 CSS 的布局机制(盒模型、盒子定位、浮动、Flexbox、Grid)问题总结大全
前端·javascript·css·vue.js·react.js·前端框架·html
Skrrapper2 小时前
【三大前端语言之一】静态网页语言:HTML详解
前端·html