事件循环,宏任务,微任务

一、为什么需要事件循环?

JavaScript 是单线程的,意思是它一次只能做一件事。如果所有任务都排队等着,遇到一个耗时任务(比如网络请求、定时器、读取大文件),整个页面就会卡住,什么都点不了。这显然不行。

所以 JavaScript 设计了异步机制 :先执行当前任务,把耗时的任务交给浏览器(或 Node.js)去处理,等任务完成后再回头执行对应的回调。这个"回头执行"的协调机制,就是事件循环(Event Loop)


二、事件循环怎么工作?

可以把事件循环想象成一个永不停止的工厂流水线 ,它不断从任务队列 里取出任务放到主线程执行。但任务不是随便拿的,它们分成两种:宏任务微任务

1. 宏任务(MacroTask)

  • 比较"大"的任务,通常由宿主环境(浏览器、Node)发起。
  • 常见例子:
    • script 整体代码(第一个宏任务)
    • setTimeout / setInterval 的回调
    • DOM 事件回调(如点击、键盘)
    • I/O 操作(文件读写、网络请求)
    • setImmediate(Node 独有)

2. 微任务(MicroTask)

  • 比较"小"的任务,通常由 JavaScript 引擎自身产生,优先级更高。
  • 常见例子:
    • Promise.then / catch / finally 的回调
    • MutationObserver(浏览器监听 DOM 变化)
    • queueMicrotask(手动添加微任务)
    • process.nextTick(Node 独有,优先级最高)

三、执行顺序:先微后宏,一次一个宏

事件循环的每一轮(tick)是这样的:

  1. 执行一个宏任务 (最开始是 script 整体代码)。
  2. 执行过程中,如果遇到微任务 (比如 Promise.then),就把它们放进"微任务队列"。
  3. 当前宏任务执行完毕 ,立刻清空微任务队列:依次执行所有微任务,如果在执行微任务时又产生了新的微任务,继续执行,直到微任务队列为空。
  4. 可能执行 UI 渲染(浏览器环境,视情况而定)。
  5. 从宏任务队列中取出下一个宏任务,重复以上步骤。

关键点 :微任务会在本轮宏任务结束后、下一个宏任务开始前全部执行完。所以微任务的优先级比宏任务高。


四、一个例子让你秒懂

javascript 复制代码
console.log('1'); // 同步代码,属于第一个宏任务

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

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

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

// 输出顺序:1, 4, 3, 2

解析

  • 第一个宏任务(script 整体代码)开始:
    • 输出 1
    • 遇到 setTimeout,回调被放进宏任务队列
    • 遇到 Promise.then,回调被放进微任务队列
    • 输出 4
  • 第一个宏任务结束,检查微任务队列,执行所有微任务:
    • 输出 3(微任务)
  • 微任务队列清空,取出下一个宏任务(setTimeout 回调):
    • 输出 2

五、为什么要有宏任务和微任务?

  • 宏任务 让异步任务能排队等待,不至于阻塞主线程。
  • 微任务 则提供了一种更"紧急"的异步方式,比如 Promise 回调需要尽快执行,避免不必要的延迟。这也保证了 Promise 的回调能在当前任务结束后、下一个任务开始前立即执行。

六、一个更复杂的例子

javascript 复制代码
console.log('start');

setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise3');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
  setTimeout(() => {
    console.log('timeout2');
  }, 0);
});

Promise.resolve().then(() => {
  console.log('promise2');
});

console.log('end');

// 输出顺序:start, end, promise1, promise2, timeout1, promise3, timeout2

步骤拆解

  1. 执行第一个宏任务(script):输出 start,把 setTimeout 回调加入宏任务队列,把两个 Promise.then 加入微任务队列,输出 end
  2. 清空微任务队列:依次输出 promise1promise2。在执行 promise1 的回调时,又遇到 setTimeout,将其回调加入宏任务队列(此时宏任务队列已有 timeout1)。
  3. 微任务队列清空,取下一个宏任务:执行 timeout1 回调,输出 timeout1,同时把 promise3 加入微任务队列。
  4. 当前宏任务结束,立即清空微任务队列:输出 promise3
  5. 再取下一个宏任务:执行 timeout2,输出 timeout2

七、通俗比喻

可以把事件循环想象成一个银行柜台

  • 宏任务就是来办业务的客户,每次只能接待一个(执行一个宏任务)。
  • 微任务 是客户在办理业务时临时想起的"小事"(比如签个字、填个表),这些小事必须在这个客户办完业务离开前立即处理完,不能等到下一个客户来。
  • 所以每个客户(宏任务)办完,柜员会先处理完他所有的零碎小事(微任务),才叫下一位。

八、总结

  • 事件循环 是 JavaScript 处理异步任务的核心机制。
  • 任务分两类:宏任务 (大任务,由宿主发起)和微任务(小任务,由 JS 发起)。
  • 执行顺序:每轮循环先执行一个宏任务,然后清空所有微任务,接着可能渲染,再取下一个宏任务。
  • 微任务优先级高于宏任务,保证了 Promise 等回调的及时性。
相关推荐
weixin1997010801616 小时前
《电子元器件商品详情页前端性能优化实战》
前端·性能优化
Southern Wind16 小时前
Vue 3 + Naive UI 企业级后台管理系统完整解析
前端·vue.js·ui·typescript
清汤饺子16 小时前
AI 编程新范式:Spec First 的四件套,让 AI 不再是"热情但跑偏的实习生"
前端·javascript·后端
weixin1997010801616 小时前
《建材网商品详情页前端性能优化实战》
前端·性能优化
坐吃山猪16 小时前
TypeScript编程04-函数
javascript·ubuntu·typescript
LXXgalaxy17 小时前
小程序文件上传怎么做?一套可复用的 UniApp 上传+预览 Demo
javascript·vue.js·uni-app
程序员 沐阳17 小时前
从缓慢等待到瞬间响应:Vite 如何重塑前端开发体验
前端·前端框架
wangjinsheng59317 小时前
Vue3 + Element Plus 前端 AI 编码模板
前端·vue.js·ai·elementui·ai编程
roman_日积跬步-终至千里17 小时前
【后端】Spring Boot Web请求核心问题解析
前端·spring boot·后端·系统架构
Mintopia17 小时前
让开发效率翻倍的,往往不是新技术,而是小工具
前端