【DeepSeek帮我准备前端面试100问】(基础一):宏任务,微任务,Event Loop

20 阅读5分钟

宏任务、微任务与Event Loop深度解析

一、JavaScript运行机制基础

JavaScript是一门单线程语言,这意味着它一次只能执行一个任务。这种设计避免了多线程环境中的复杂同步问题,但也带来了如何高效处理异步操作的挑战。

为什么需要异步机制?

  • 防止阻塞:如果所有操作都是同步的,长时间运行的任务会冻结整个页面
  • 提高效率:可以同时处理多个操作(如网络请求、定时器等)
  • 更好的用户体验:保持界面响应流畅

二、宏任务(Macro Task)

定义

宏任务是JavaScript引擎在每个事件循环周期中执行的主要任务单位。它们构成了事件循环的基本骨架。

常见宏任务类型

  1. script整体代码:整个<script>标签或JS文件的执行
  2. DOM操作:如appendChild等(虽然执行很快但属于宏任务)
  3. UI渲染:浏览器重绘/回流
  4. 定时器
    • setTimeout
    • setInterval
  5. I/O操作
    • 文件读写(Node.js)
    • 网络请求回调
  6. 事件回调
    • 鼠标点击
    • 键盘事件
  7. 特殊API
    • setImmediate(Node.js特有)
    • requestAnimationFrame(浏览器特有)
    • MessageChannel

执行特点

  • 每个事件循环周期执行一个宏任务(准确说是执行到清空调用栈)
  • 宏任务队列遵循先进先出(FIFO)原则
  • 浏览器可以来自不同任务源(如定时器、IO等)的宏任务分开存储

三、微任务(Micro Task)

定义

微任务是在当前宏任务执行结束后立即执行的任务,它们具有更高的优先级,会在下一个宏任务开始前全部执行完毕。

常见微任务类型

  1. Promise回调
    • then()
    • catch()
    • finally()
  2. MutationObserver:监听DOM变化
  3. 特定API
    • queueMicrotask(专门的微任务API)
    • process.nextTick(Node.js特有,优先级最高)

执行特点

  • 在当前宏任务之后、下一个宏任务之前执行
  • 必须完全清空微任务队列才会继续事件循环
  • 微任务可以嵌套产生新的微任务(浏览器通常有保护机制防止无限循环)
  • 执行时机比requestAnimationFrame更早

四、Event Loop完整机制

核心组件

  1. 调用栈(Call Stack):存储函数调用的栈结构
  2. 任务队列(Task Queue)
    • 宏任务队列(可能有多个,如定时器队列、IO队列等)
    • 微任务队列(通常只有一个)
  3. Web APIs:浏览器提供的异步API环境
  4. 渲染引擎:处理页面布局和绘制

详细执行流程

  1. 初始执行

    • 整个script作为第一个宏任务执行
    • 同步代码直接进入调用栈执行
  2. 遇到异步操作

    setTimeout(() => { console.log('timeout') }, 0)
    Promise.resolve().then(() => { console.log('promise') })
    
    • setTimeout:交给Web API计时,到期后回调进入宏任务队列
    • Promise.then:回调进入微任务队列
  3. 当前宏任务结束

    • 检查微任务队列并执行所有微任务
    • 如果微任务又产生新的微任务,继续执行直到队列为空
  4. 渲染时机(浏览器):

    • 检查是否需要渲染(约60fps)
    • 执行requestAnimationFrame回调
    • 布局(Layout)和绘制(Paint)
  5. 下一轮循环

    • 从宏任务队列取出最老的任务执行
    • 重复上述过程

关键特性

  • 微任务优先:总是先执行完所有微任务才考虑下一个宏任务
  • 任务交错:浏览器可能在多个宏任务之间进行渲染
  • 饥饿问题:如果微任务不断产生新微任务,会阻塞宏任务执行

五、Node.js与浏览器差异

特性浏览器Node.js
微任务类型Promise, MutationObserverPromise, process.nextTick
定时器精度约4ms的最小延迟约1ms的最小延迟
setImmediate不存在有,在check阶段执行
任务队列管理通常单个宏任务队列多个阶段(timers、poll等)
process.nextTick不存在有,优先级最高

六、经典面试题分析

题目1:基础执行顺序

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

输出顺序

  1. script start
  2. script end
  3. promise1
  4. promise2
  5. setTimeout

解析

  1. 同步代码首先执行
  2. setTimeout回调进入宏任务队列
  3. Promise回调进入微任务队列
  4. 同步代码执行完后立即执行所有微任务
  5. 最后执行下一个宏任务

题目2:混合任务嵌套

setTimeout(() => console.log("timeout1"));

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

Promise.resolve()
  .then(() => {
    console.log("promise2");
    setTimeout(() => console.log("timeout3"));
  });

setTimeout(() => console.log("timeout4"));

输出顺序

  1. promise1
  2. promise2
  3. timeout1
  4. timeout4
  5. timeout2
  6. timeout3

解析

  1. 首先执行所有微任务(promise1/promise2)
  2. 微任务中的setTimeout进入下一轮宏任务队列
  3. 执行初始的宏任务(timeout1/timeout4)
  4. 最后执行微任务中添加的宏任务(timeout2/timeout3)

七、实际应用注意事项

  1. 避免微任务无限循环

    function infiniteMicrotask() {
      Promise.resolve().then(infiniteMicrotask);
    }
    
    • 这会完全阻塞主线程
  2. 合理分配任务类型

    • 对实时性要求高的用微任务
    • 大量计算用宏任务分批次执行
  3. Node.js特殊处理

    setImmediate(() => console.log('immediate'));
    setTimeout(() => console.log('timeout'), 0);
    
    • 在I/O周期内,setImmediate总是先执行
    • 否则顺序不确定
  4. 渲染优化

    • 大量DOM操作应该用requestAnimationFrame
    • 避免在微任务中进行昂贵计算影响渲染

八、现代API扩展

  1. queueMicrotask

    queueMicrotask(() => {
      console.log('This runs as a microtask');
    });
    
    • 比Promise更直接的微任务API
  2. MutationObserver

    const observer = new MutationObserver(() => {
      console.log('DOM changed');
    });
    observer.observe(targetNode, { attributes: true });
    
    • 用于监听DOM变化的微任务接口
  3. requestIdleCallback

    requestIdleCallback(() => {
      console.log('Run during idle periods');
    });
    
    • 在浏览器空闲时执行的低优先级任务

理解这些概念可以帮助开发者:

  • 优化代码执行顺序
  • 避免常见的异步陷阱
  • 构建更高效的Web应用
  • 更好地调试异步代码问题