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

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

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 等回调的及时性。
相关推荐
z止于至善2 小时前
Vue ECharts:Vue 生态下的 ECharts 可视化最佳实践
前端·vue.js·echarts·vue echarts
℘团子এ2 小时前
什么是Docker
前端·docker·容器
Software攻城狮2 小时前
【el-table 表格组件 删除标头分割线】
前端·vue.js·elementui
陆康永2 小时前
vue2封装hook函数,可以监听主页面生命周期
前端·javascript·vue.js
我命由我123452 小时前
Vue Router - 记录一下 2 种路由写法
前端·javascript·vue.js·前端框架·html·html5·js
m0_719084112 小时前
导入导出—设备管理系统
前端·javascript·vue.js
周淳APP2 小时前
【计算机网络之XSS、CSRF、DDoS原理及防御措施】
前端·网络·计算机网络·http·ddos·xss·csrf
wuhen_n2 小时前
Vue Router 进阶:路由懒加载、导航守卫与元信息的高效运用
前端·javascript·vue.js
SoaringHeart2 小时前
Flutter进阶|源码修改:给 DecorationImage 源码添加偏移量
前端·flutter