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

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

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. 清空微任务队列:依次输出 promise1、promise2。在执行 promise1 的回调时,又遇到 setTimeout,将其回调加入宏任务队列(此时宏任务队列已有 timeout1)。
  3. 微任务队列清空,取下一个宏任务:执行 timeout1 回调,输出 timeout1,同时把 promise3 加入微任务队列。
  4. 当前宏任务结束,立即清空微任务队列:输出 promise3。
  5. 再取下一个宏任务:执行 timeout2,输出 timeout2。

七、通俗比喻

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

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

八、总结

  • 事件循环 是 JavaScript 处理异步任务的核心机制。
  • 任务分两类:宏任务 (大任务,由宿主发起)和微任务(小任务,由 JS 发起)。
  • 执行顺序:每轮循环先执行一个宏任务,然后清空所有微任务,接着可能渲染,再取下一个宏任务。
  • 微任务优先级高于宏任务,保证了 Promise 等回调的及时性。
相关推荐
沙漠之主13 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty13 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js
Csvn13 小时前
diff 算法(虚拟 DOM Reconciliation)
前端
前端snow14 小时前
ai agent --- 文件存储
前端
Frag0ut14 小时前
Chrome四大版本获取及共存指南:Stable/Beta/Dev/Canary
前端·chrome·浏览器·dev·beta·canary·共存版
IT_陈寒16 小时前
SpringBoot自动配置坑了我一把,原来是这样绕过去的
前端·人工智能·后端
广州华水科技16 小时前
单北斗GNSS变形监测在大坝安全监测中的应用与优势
前端
zeng不错16 小时前
B站 173 个投稿活动看到眼瞎?我写了个扩展,主打一个精准打击
前端
Latchh16 小时前
前端导出的JPEG红字发糊,质量要拉到100才清楚
前端·图像处理·人工智能·计算机视觉
默_笙17 小时前
🚦 请三个 AI 考官给 RAG 打分:量化评估实战(下)
前端·javascript