JavaScript事件循环:从火锅店经营到并发大师

31 阅读4分钟

JavaScript事件循环:从火锅店经营到并发大师

痛点暴击 🥘

"说说JS的事件循环机制?"

当面试官抛出这个问题时,你的大脑是否像火锅店高峰期:

  • 订单(任务)不断涌入
  • 服务员(主线程)手忙脚乱
  • 后厨(Web API)压力山大
  • 传菜员(回调队列)来回奔波

最后呈现给客人的却是:未煮熟的毛肚(未处理的任务)和煮过头的脑花(阻塞的UI)...

跨界破冰:事件循环就像火锅店 🍲

场景映射:

class 火锅店 {
  constructor() {
    this.大堂 = [];       // 调用栈
    this.后厨 = new Map(); // Web APIs
    this.传菜区 = [];     // 任务队列
  }

  接待(顾客) {           // 主线程
    this.处理订单(顾客);
    this.检查后厨();
    this.上菜循环();
  }
}

核心员工职责:

  1. 大堂经理(调用栈):现场处理简单订单
  2. 后厨团队(Web API):处理耗时操作(setTimeout等)
  3. 传菜小哥(事件循环):不断检查后厨完成情况

毒性技术拆解 🔥

1. 经典面试题翻车现场

console.log('锅底'); // 同步任务

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

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

console.log('蘸料'); 
// 输出顺序:锅底 → 蘸料 → 毛肚 → 肥牛

2. 九大诡异现象(附解法)

1. setTimeout(fn, 0) 真的0秒执行?
   - 真相:至少4ms的浏览器限制

2. 点击事件比setTimeout先触发?
   - 原理:事件冒泡属于宏任务

3. 为什么Promise.then比setTimeout快?
   - 机制:微任务优先级更高

4. requestAnimationFrame在渲染前还是后执行?
   - 冷知识:就像火锅加汤,总是在渲染前执行

5. 为什么postMessage有时比setTimeout快?
   - 秘密:跨文档通信有VIP通道(任务队列不同)

6. Web Worker中的setTimeout会阻塞主线程吗?
   - 原理:像分店后厨,独立运作互不影响

7. 微任务会饿死宏任务吗?
   - 案例:无限递归Promise.then会让页面卡死

8. 点击事件和setTimeout谁先执行?
   - 规则:像叫号系统,先到先得(取决于触发时机)

9. 为什么async函数比setTimeout优先?
    - 本质:async返回Promise,属于微任务

增强演示:事件循环运转图 🎡

三维运转模型

  ┌───────────────────────┐
  │       宏任务队列        │ ← click事件
  └──────────┬────────────┘
             │
  ┌──────────▼────────────┐
  │     调用栈(大堂)      │ ← 正在执行脚本
  └──────────┬────────────┘
             │
  ┌──────────▼────────────┐
  │       微任务队列        │ ← Promise.then
  └──────────┬────────────┘
             │
  ┌──────────▼────────────┐
  │  渲染管道(上菜窗口)    │ ← requestAnimationFrame
  └───────────────────────┘

动态演示工具

// 在浏览器控制台运行观察
function 压力测试() {
  setTimeout(() => console.log('宏任务1'), 0);
  
  Promise.resolve().then(() => {
    console.log('微任务1');
    Promise.resolve().then(() => console.log('嵌套微任务'));
  });

  requestAnimationFrame(() => console.log('动画帧回调'));
}

压力测试();
// 输出顺序:微任务1 → 嵌套微任务 → 动画帧回调 → 宏任务1

实战进阶:高并发涮肉术 🥢

案例1:避免汤底烧干(防止阻塞)

// 错误示范:同步计算阻塞
function 切肉() {
  let i = 0;
  while(i++ < 1000000000); // 同步阻塞
}

// 正确做法:分片处理
function 高效切肉() {
  function 切片(start) {
    if(start >= 1000000) return;
    requestIdleCallback(() => {
      while(start < start + 1000) { /* 处理切片 */ }
      切片(start + 1000);
    });
  }
  切片(0);
}

案例2:智能上菜系统(优先级控制)

class 任务调度器 {
  constructor() {
    this.微任务队列 = [];
    this.宏任务队列 = [];
  }

  加菜(任务, 类型) {
    (类型 === '微' ? this.微任务队列 : this.宏任务队列).push(任务);
  }

  上菜() {
    while(this.微任务队列.length) {
      const 任务 = this.微任务队列.shift();
      任务();
    }

    if(this.宏任务队列.length) {
      const 任务 = this.宏任务队列.shift();
      setTimeout(任务, 0);
    }
  }
}

灵魂暴击10连问 💥

  1. process.nextTick与Promise.then的优先级差异?
  2. 如何实现一个微任务polyfill?
  3. Node.js与浏览器事件循环的核心区别?
  4. requestAnimationFrame属于哪类任务?
  5. 为什么MutationObserver是微任务?
  6. 如何检测主线程阻塞?
  7. 宏任务之间的执行顺序规则?
  8. Web Worker如何影响事件循环?
  9. 如何实现任务优先级反转?
  10. 事件循环与垃圾回收的关系?

面试反杀菜谱 🥗

当面试官深入追问时,展示这道"硬菜":

console.log('锅底');

setTimeout(() => {
  console.log('香菜');
  Promise.resolve().then(() => console.log('葱花'));
}, 0);

Promise.resolve().then(() => {
  console.log('香油');
  setTimeout(() => console.log('蒜末'), 0);
});

requestAnimationFrame(() => console.log('加汤'));

// 正确输出顺序:锅底 → 香油 → 加汤 → 香菜 → 葱花 → 蒜末

防忘涮肉口诀 🧠

  1. 同步任务马上涮
  2. 微任务像小料台
  3. 宏任务等叫号
  4. 渲染前要加汤
  5. 嵌套任务像拼盘

免责声明:本文部分内容由AI生成,技术细节仅供参考。实际开发请以各运行时环境为准。

下期预告:CSS布局原理:从俄罗斯方块到乐高积木的排列艺术