🌟 JavaScript事件循环:单线程的异步魔法秀 🎪

62 阅读5分钟

朋友们,想象一下你是个忙碌的咖啡师👨‍🍳,既要接单又要做咖啡,但只有一双手!这就是JavaScript的处境——单线程执行,一次只能做一件事。那么它如何同时处理点击事件、网络请求和动画呢?答案就是事件循环(Event Loop) 这个幕后魔术师!

🧩 为什么需要Event Loop?

JavaScript生来就是单线程的(就像咖啡师只有一双手)。如果所有任务都同步执行:

// 同步地狱示例
downloadHugeFile(); // 卡住10秒
handleUserClick(); // 用户疯狂点击无响应
animateElement(); // 动画冻结

大家都知道我们前端是十分注重用户体验的,一但用户会看到"页面无响应"的提示😱!那么会直接关闭我们的网页,影响我们网页的访问量,Event Loop的诞生就是为了解决这个问题——让耗时不阻塞关键操作,就像咖啡师让磨豆机自己工作,先去服务顾客。

🎭 Event Loop 剧场:三幕剧

通过我们今天的代码案例,揭秘事件循环的三幕大戏:

每一个script就是一个宏任务,里面有同步任务和异步任务,异步任务又分为宏任务微任务,当同步任务执行完就开始执行所有异步任务的微任务,执行完后才开始执行其它宏任务,所以微任务是异步任务里面的vip

三者关系如下图,执行完毕指的是所有的同步任务执行完毕 0c096ade76bd5e085540349be903141c.png

第一幕:同步任务闪电战 ⚡

<script>
  console.log('script start'); // 【同步】立即执行
  console.log('script end');   // 【同步】立即执行
</script>

同步代码像闪电侠🏃‍♂️,总是最先冲过终点线。执行顺序:

image.png

script start → script end

第二幕:微任务插队优先证 🎫

<script>
  Promise.resolve().then(() => {
    console.log('promise1'); // 【微任务】插队执行!
  });
</script>

微任务就像VIP客户,总是插队到下一个宏任务之前,但依旧在同步任务之后

console.log('同步Start');
Promise.resolve().then(() => console.log('First Promise')); 
console.log('同步End');

// 输出:
// 同步Start → 同步End → First Promise

image.png

第三幕:宏任务耐心排队 🚶‍♂️🚶‍♀️

<script>
  setTimeout(() => {
    console.log('setTimeout'); // 【宏任务】老实排队
  }, 0);
</script>

image.png

宏任务像普通顾客,规规矩矩排队,只有当所有的同步任务和微任务执行完,才能执行下一个宏任务,每一个宏任务有自己的执行上下文,又会按照上述event loop重复步骤

console.log('script start');
        Promise.resolve().then(() => console.log('First Promise'));
        setTimeout(() => {
            console.log('setTimeout satrt'); // 【宏任务】老实排队
            Promise.resolve().then(() => console.log('Second Promise')); // 【微任务】优先执行
            console.log('setTimeout end');
        }, 0);
        setTimeout(() => {
            console.log('setTimeout2'); // 【宏任务】老实排队
        }, 0)
        console.log('script end');

可以看到第一个定时器,创建一个属于自己的执行上下文,里面有属于自己的同步任务,微任务,当然也可以有宏任务 image.png

setTimeout(() => console.log('下一个宏任务'));
setTimeout(() => console.log('下下一个宏任务'));

// 输出:
// 下一个宏任务 → 下下一个宏任务

🎪 任务类型大揭秘

🟢 微任务(VIP通道)

- `promise.then()` → 异步之王
- `MutationObserver` → DOM侦探
- `queueMicrotask` → 微任务直通车
- `process.nextTick` → Node.js特快

MutationObserver 现场秀 👓

<script>
  const target = document.createElement('div');
  const observer = new MutationObserver(() => {
    console.log('微任务: MutationObserver'); // DOM变更立即捕获!
  });
  
  target.setAttribute('class', 'test');      // 触发1次
  target.appendChild(document.createElement('span')); // 再触发?
  target.setAttribute('style', 'color:red'); // 又触发?
</script>

神奇的是:多次DOM修改只触发一次回调!因为浏览器会把同事件循环内的DOM变更打包处理。

其实就是批处理了,类似于useState解构出来的响应式改变状态的函数,如果多次setState(state + 1)也会批处理为一次,页面只+1

queueMicrotask 直通车 🚀

<script>
  console.log('同步');
  queueMicrotask(() => console.log('微任务: queueMicrotask'));
  console.log('同步结束');
</script>

输出永远是:

同步 → 同步结束 → 微任务: queueMicrotask

🔵 宏任务(普通通道)

- `script`(整体代码)→ 开场大戏
- `setTimeout/setInterval` → 定时器代表
- `I/O操作` → 文件/网络请求
- `UI渲染` → 重绘重排

🎯 Node.js特别场:process.nextTick

console.log('Start'); 
Promise.resolve().then(() => console.log('Promise Resolved'));
process.nextTick(() => console.log('Process Next Tick'));
console.log('end');


/* 输出顺序:
   Start
   end
   Process Next Tick → Node.js微任务之王!
   Promise Resolved
*/

image.png

在Node.js中,process.nextTick优先级甚至高于Promise,是真正的微任务VIP中P

🌈 终极挑战:混合任务大乱斗

console.log('同步Start');
const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const pormise3 = new Promise(resolve => {
    // 同步的,所以先打印promise3,然后打印同步End
    // 只有.then()才是异步的,是微任务
    console.log('promise3');
    resolve('Third Promise');
})

promise1.then(value => {
    console.log(value);
})
promise2.then(value => {
    console.log(value);
})
pormise3.then(value => {
    console.log(value);

})
setTimeout(() => {
    console.log('下一个宏任务');
    const promise4 = Promise.resolve('Fourth Promise');
    promise4.then(value => {
        console.log(value);
    })
}, 0)
setTimeout(() => {
    console.log('下下一个宏任务');
}, 0)

console.log('同步End');

image.png

关键规律:

  1. 同步代码永远最先执行
  2. 微任务清空前,下一个宏任务休想启动
  3. 每个宏任务都自带微任务队列

💡 为什么这样设计?

想象一个场景:你修改了DOM后想立即获取元素尺寸:

button.style.width = '200px';
console.log(button.offsetWidth); // 需要最新值!

如果这时UI渲染(宏任务)插队,获取的将是旧值!并且会引起新的重排,消耗新能,因此:

  1. 同步代码修改DOM
  2. 微任务中可获取未渲染的DOM变更
  3. 宏任务执行前先执行UI渲染

这就是为什么MutationObserver被设计为微任务——在渲染前捕获DOM变更!

🚀 总结:Event Loop 三定律

  1. 同步优先:主线程代码一路狂奔到底
  2. 微次之:Promise、MutationObserver等紧随其后
  3. 宏最后:setTimeout、渲染等压轴出场

记住这个永恒公式:

同步 → 微任务 → 渲染 → 宏任务 → (微任务 again)

最后附表,这里展示了一下常见的宏任务和微任务

🔮 JavaScript 任务类型速查表

任务类型具体API执行时机环境趣味解读
🔴 宏任务script标签最先执行通用"程序入口,开胃菜"
setTimeout下一轮循环通用"闹钟叫醒的拖延症患者" ⏰
setInterval周期性执行通用"永动机(直到clear)" ♾️
I/O操作回调阶段通用"快递小哥送货上门" 📦
UI渲染微任务后浏览器"舞台灯光师" 💡
事件监听事件触发时浏览器"随叫随到的服务员" 🛎️
🟢 微任务Promise.then同步结束立即执行通用"VIP快速通道" 💎
queueMicrotask同步结束立即执行浏览器"微任务直通车" 🚄
MutationObserverDOM变更后浏览器"DOM私家侦探" 🔍
process.nextTick当前循环末尾Node.js"Node插队王" 👑
Promise.catch同步结束立即执行通用"错误处理急先锋" 🚨