前端事件循环:宏任务与微任务的深度解析

你以为JavaScript是单线程的,但它却用事件循环实现了"伪异步"。理解宏任务和微任务,是掌握现代前端异步编程的关键。

引言:从一道经典面试题说起

javascript

复制代码
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

// 输出顺序是什么?

如果你的答案是"1, 4, 3, 2",那么恭喜你已经理解了事件循环的基本概念。但事件循环远不止于此...

第一部分:JavaScript运行环境的真相

1.1 为什么JavaScript是单线程的?

JavaScript最初被设计为浏览器脚本语言,主要用于处理DOM操作。多线程同时操作DOM会带来复杂的同步问题。因此,JavaScript采用了单线程+事件循环的模型。

javascript

复制代码
// 浏览器中的JavaScript执行环境
┌───────────────────────────┐
│         JavaScript        │  ← 单线程执行
│   Engine (V8/SpiderMonkey)│
└───────────────────────────┘
           ↑    ↓
┌───────────────────────────┐
│     Web APIs (浏览器提供)   │  ← 异步API:setTimeout、DOM事件、Ajax等
└───────────────────────────┘
           ↑    ↓
┌───────────────────────────┐
│    Task Queue (任务队列)   │  ← 待执行的回调函数
└───────────────────────────┘

1.2 事件循环的基本原理

javascript

复制代码
// 事件循环的简化模型
while (eventLoop.waitForTask()) {
  // 1. 从任务队列中取出一个任务
  const task = eventLoop.getNextTask();
  
  // 2. 执行任务
  try {
    task();
  } catch (error) {
    console.error('任务执行出错:', error);
  }
  
  // 3. 执行所有微任务
  eventLoop.processMicrotasks();
  
  // 4. 渲染(如果需要)
  if (shouldRender()) {
    eventLoop.render();
  }
}

第二部分:宏任务 vs 微任务

2.1 什么是宏任务?

宏任务(MacroTask)代表一个独立的、完整的工作单元。每个宏任务执行完后,浏览器可能会进行渲染。

常见的宏任务:

  • script(整体代码)

  • setTimeout / setInterval

  • setImmediate(Node.js)

  • I/O操作

  • UI渲染(浏览器)

  • 事件回调(click、load等)

  • MessageChannel

javascript

复制代码
// 宏任务示例
console.log('脚本开始'); // 这是第一个宏任务

setTimeout(() => {
  console.log('setTimeout回调'); // 新的宏任务
}, 0);

button.addEventListener('click', () => {
  console.log('按钮点击'); // 事件回调是宏任务
});

// 当前宏任务结束

2.2 什么是微任务?

微任务(MicroTask)是在当前宏任务结束后、下一个宏任务开始前立即执行的任务。微任务队列会在每个宏任务执行完毕后清空。

常见的微任务:

  • Promise.then / .catch / .finally

  • async/await(本质是Promise)

  • MutationObserver(浏览器)

  • process.nextTick(Node.js,优先级最高)

  • queueMicrotask API

javascript

复制代码
// 微任务示例
console.log('开始');

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

queueMicrotask(() => {
  console.log('queueMicrotask'); // 微任务
});

console.log('结束');

// 输出:开始 → 结束 → Promise 1 → Promise 2 → queueMicrotask

2.3 完整的执行顺序

javascript

复制代码
// 完整的事件循环顺序示例
console.log('1 - 同步代码(宏任务开始)');

setTimeout(() => {
  console.log('2 - setTimeout(宏任务)');
  Promise.resolve().then(() => {
    console.log('3 - 内层Promise(微任务)');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('4 - 外层Promise(微任务)');
  setTimeout(() => {
    console.log('5 - 内层setTimeout(宏任务)');
  }, 0);
});

console.log('6 - 同步代码(宏任务结束)');

// 执行顺序分析:
// 1. 执行当前宏任务(整体代码):输出 1, 6
// 2. 执行微任务队列:输出 4
// 3. 执行下一个宏任务(第一个setTimeout):输出 2
// 4. 执行该宏任务产生的微任务:输出 3
// 5. 执行下一个宏任务(第二个setTimeout):输出 5

第三部分:浏览器与Node.js的事件循环差异

3.1 浏览器的事件循环模型

javascript

复制代码
// 浏览器事件循环阶段
┌───────────────────────┐
│      宏任务队列        │
│ 1. 执行一个宏任务       │
└──────────┬────────────┘
           │
┌──────────▼────────────┐
│      微任务队列        │
│ 2. 执行所有微任务       │
└──────────┬────────────┘
           │
┌──────────▼────────────┐
│      requestAnimation │
│ 3. 执行RAF回调         │
└──────────┬────────────┘
           │
┌──────────▼────────────┐
│       渲染阶段         │
│ 4. 样式计算、布局、绘制 │
└──────────┬────────────┘
           │
┌──────────▼────────────┐
│   requestIdleCallback │
│ 5. 执行RIC回调(空闲时)│
└───────────────────────┘

3.2 Node.js的事件循环模型

javascript

复制代码
// Node.js事件循环阶段(更复杂)
   ┌───────────────────────────┐
   │        timers阶段         │ ← 执行setTimeout/setInterval回调
   └─────────────┬─────────────┘
                 │
   ┌─────────────▼─────────────┐
   │   pending callbacks阶段   │ ← 执行上一轮未执行的I/O回调
   └─────────────┬─────────────┘
                 │
   ┌─────────────▼─────────────┐
   │     idle, prepare阶段     │ ← 内部使用
   └─────────────┬─────────────┘
                 │
   ┌─────────────▼─────────────┐
   │       poll阶段            │ ← 检索新的I/O事件,执行相关回调
   └─────────────┬─────────────┘
                 │
   ┌─────────────▼─────────────┐
   │      check阶段            │ ← 执行setImmediate回调
   └─────────────┬─────────────┘
                 │
   ┌─────────────▼─────────────┐
   │   close callbacks阶段     │ ← 执行close事件的回调
   └───────────────────────────┘

3.3 关键差异对比

javascript

复制代码
// 差异1:process.nextTick vs Promise
Promise.resolve().then(() => {
  console.log('Promise');
});

process.nextTick(() => {
  console.log('nextTick'); // 先执行
});

// 差异2:setTimeout vs setImmediate
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate'); // 顺序不确定,取决于当前执行环境
});

// 差异3:浏览器 vs Node.js的微任务执行时机
setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('promise1');
  });
}, 0);

setTimeout(() => {
  console.log('timeout2');
  Promise.resolve().then(() => {
    console.log('promise2');
  });
}, 0);

// 浏览器输出:timeout1 → promise1 → timeout2 → promise2
// Node.js可能输出:timeout1 → timeout2 → promise1 → promise2

第四部分:实战应用场景

4.1 性能优化:避免阻塞渲染

javascript

复制代码
// 不好:大量同步任务阻塞渲染
function processLargeArray(array) {
  const results = [];
  for (let i = 0; i < array.length; i++) {
    // 昂贵的计算
    results.push(expensiveCalculation(array[i]));
  }
  return results;
}

// 好:将任务分解为多个宏任务
function processLargeArrayAsync(array, chunkSize = 100) {
  return new Promise((resolve) => {
    const results = [];
    let index = 0;
    
    function processChunk() {
      const end = Math.min(index + chunkSize, array.length);
      
      for (; index < end; index++) {
        results.push(expensiveCalculation(array[index]));
      }
      
      if (index < array.length) {
        // 使用setTimeout让出控制权,允许渲染
        setTimeout(processChunk, 0);
      } else {
        resolve(results);
      }
    }
    
    processChunk();
  });
}

// 更好:使用微任务避免不必要的渲染
function processLargeArrayMicrotask(array, chunkSize = 100) {
  return new Promise((resolve) => {
    const results = [];
    let index = 0;
    
    function processChunk() {
      const end = Math.min(index + chunkSize, array.length);
      
      for (; index < end; index++) {
        results.push(expensiveCalculation(array[index]));
      }
      
      if (index < array.length) {
        // 使用queueMicrotask,在当前任务结束后立即执行
        queueMicrotask(processChunk);
      } else {
        resolve(results);
      }
    }
    
    processChunk();
  });
}

4.2 实现优先级调度

javascript

复制代码
class TaskScheduler {
  constructor() {
    this.microTasks = [];
    this.macroTasks = [];
    this.isProcessing = false;
  }
  
  // 添加高优先级任务(微任务)
  addMicrotask(task) {
    this.microTasks.push(task);
    this.scheduleRun();
  }
  
  // 添加普通任务(宏任务)
  addMacrotask(task) {
    this.macroTasks.push(task);
    this.scheduleRun();
  }
  
  scheduleRun() {
    if (this.isProcessing) return;
    
    this.isProcessing = true;
    
    // 使用微任务来启动处理
    queueMicrotask(() => {
      this.processTasks();
    });
  }
  
  processTasks() {
    // 先处理所有微任务
    while (this.microTasks.length > 0) {
      const task = this.microTasks.shift();
      try {
        task();
      } catch (error) {
        console.error('微任务执行失败:', error);
      }
    }
    
    // 然后处理一个宏任务
    if (this.macroTasks.length > 0) {
      const task = this.macroTasks.shift();
      try {
        task();
      } catch (error) {
        console.error('宏任务执行失败:', error);
      }
    }
    
    // 如果还有任务,继续调度
    if (this.microTasks.length > 0 || this.macroTasks.length > 0) {
      this.scheduleRun();
    } else {
      this.isProcessing = false;
    }
  }
}

// 使用示例
const scheduler = new TaskScheduler();

scheduler.addMacrotask(() => console.log('宏任务 1'));
scheduler.addMicrotask(() => console.log('微任务 1'));
scheduler.addMacrotask(() => console.log('宏任务 2'));
scheduler.addMicrotask(() => console.log('微任务 2'));

// 输出:微任务 1 → 微任务 2 → 宏任务 1 → 宏任务 2

4.3 实现防抖与节流的升级版

javascript

复制代码
// 使用微任务优化的防抖
function debounceMicrotask(fn, delay) {
  let timerId = null;
  let microtaskQueued = false;
  
  return function(...args) {
    const context = this;
    
    // 清除之前的定时器
    if (timerId) {
      clearTimeout(timerId);
    }
    
    // 如果没有微任务在排队,创建一个
    if (!microtaskQueued) {
      microtaskQueued = true;
      
      queueMicrotask(() => {
        microtaskQueued = false;
        
        // 设置新的定时器
        timerId = setTimeout(() => {
          fn.apply(context, args);
          timerId = null;
        }, delay);
      });
    }
  };
}

// 使用示例
const expensiveSearch = debounceMicrotask((query) => {
  console.log('搜索:', query);
  // 实际搜索逻辑
}, 300);

// 快速连续输入
expensiveSearch('a');
expensiveSearch('ab');
expensiveSearch('abc'); // 只执行最后一次

4.4 React中的批量更新

javascript

复制代码
// React利用事件循环实现状态批量更新
class FakeReact {
  constructor() {
    this.state = {};
    this.isBatchingUpdates = false;
    this.pendingStates = [];
  }
  
  setState(newState) {
    if (this.isBatchingUpdates) {
      // 如果在批处理中,收集状态更新
      this.pendingStates.push(newState);
    } else {
      // 否则直接更新
      this.applyUpdate(newState);
    }
  }
  
  batchedUpdates(callback) {
    this.isBatchingUpdates = true;
    
    try {
      callback();
    } finally {
      this.isBatchingUpdates = false;
      
      // 在微任务中执行所有收集的更新
      if (this.pendingStates.length > 0) {
        queueMicrotask(() => {
          const states = [...this.pendingStates];
          this.pendingStates = [];
          
          states.forEach(state => {
            this.applyUpdate(state);
          });
        });
      }
    }
  }
  
  applyUpdate(newState) {
    this.state = { ...this.state, ...newState };
    console.log('状态更新:', this.state);
  }
}

// 使用示例
const react = new FakeReact();

react.batchedUpdates(() => {
  react.setState({ count: 1 });
  react.setState({ count: 2 });
  react.setState({ count: 3 });
});

// 只会触发一次更新:{ count: 3 }

第五部分:常见陷阱与最佳实践

5.1 微任务无限递归

javascript

复制代码
// 危险的代码:微任务无限循环
function dangerousMicrotaskLoop() {
  Promise.resolve().then(() => {
    console.log('微任务执行');
    dangerousMicrotaskLoop(); // 递归调用
  });
}

// 这会阻塞事件循环,导致页面无响应
// 因为微任务队列永远不会清空

// 安全的方式:使用宏任务
function safeMacrotaskLoop() {
  console.log('宏任务执行');
  setTimeout(safeMacrotaskLoop, 0); // 允许渲染
}

5.2 混合使用宏任务和微任务

javascript

复制代码
// 不推荐的模式
button.addEventListener('click', () => {
  // 宏任务中产生微任务
  Promise.resolve().then(() => {
    // 微任务中又产生宏任务
    setTimeout(() => {
      // 难以追踪执行顺序
      console.log('多层嵌套');
    }, 0);
  });
});

// 推荐的模式:保持清晰的任务层次
async function handleClick() {
  // 步骤1:微任务处理
  await processImmediate();
  
  // 步骤2:宏任务处理
  setTimeout(() => {
    processDelayed();
  }, 0);
}

button.addEventListener('click', handleClick);

5.3 最佳实践总结

  1. 优先使用微任务:对于需要立即执行但不阻塞渲染的任务

  2. 适时使用宏任务:对于可以延迟执行或需要允许渲染的任务

  3. 避免微任务递归:防止微任务队列永不空

  4. 合理使用async/await:理解其基于Promise(微任务)的本质

  5. 考虑使用queueMicrotask:比Promise.resolve().then()更语义化

  6. 注意执行顺序:在混合使用时要清晰了解执行顺序

第六部分:现代API与事件循环

6.1 requestAnimationFrame

javascript

复制代码
// requestAnimationFrame在渲染前执行
console.log('开始');

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

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
});

Promise.resolve().then(() => {
  console.log('Promise');
});

console.log('结束');

// 典型输出:开始 → 结束 → Promise → requestAnimationFrame → setTimeout
// 但注意:RAF在渲染前执行,时机可能因浏览器而异

6.2 requestIdleCallback

javascript

复制代码
// 在空闲时间执行低优先级任务
function processIdleTasks(deadline) {
  while (tasks.length > 0 && deadline.timeRemaining() > 0) {
    const task = tasks.shift();
    task();
  }
  
  if (tasks.length > 0) {
    requestIdleCallback(processIdleTasks);
  }
}

// 与事件循环的配合
button.addEventListener('click', () => {
  // 高优先级任务立即执行
  console.log('点击处理');
  
  // 低优先级任务在空闲时执行
  requestIdleCallback(() => {
    console.log('空闲任务');
  });
});

6.3 MutationObserver

javascript

复制代码
// MutationObserver使用微任务
const observer = new MutationObserver((mutations) => {
  console.log('DOM变化', mutations);
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

// 测试
setTimeout(() => {
  document.body.appendChild(document.createElement('div'));
  console.log('添加元素后');
}, 0);

// 输出顺序:添加元素后 → DOM变化
// MutationObserver回调作为微任务执行

总结:掌握事件循环的艺术

事件循环是JavaScript异步编程的核心机制,理解宏任务和微任务的差异对于编写高性能、响应迅速的前端应用至关重要。

关键要点:

  1. 宏任务是独立的,执行完一个宏任务后,会执行所有微任务

  2. 微任务是紧接的,在当前宏任务结束后立即执行

  3. 渲染时机:通常在微任务执行完毕后,下一个宏任务开始前

  4. 优先级:同步代码 > 微任务 > 渲染 > 宏任务

何时使用什么?

  • 微任务:需要立即执行的状态更新、Promise处理、数据同步

  • 宏任务:需要延迟执行的任务、I/O操作、用户交互处理

  • requestAnimationFrame:与渲染相关的动画、视觉更新

  • requestIdleCallback:低优先级的后台任务

记住:事件循环不是JavaScript引擎的特性,而是宿主环境(浏览器/Node.js)提供的机制。不同的宿主环境可能有不同的实现,但核心概念相通。

通过深入理解事件循环,你不仅能写出更好的异步代码,还能更有效地调试性能问题,构建更流畅的用户体验。

相关推荐
用户4445543654262 小时前
Android开发中的封装思路指导
前端
Felixwb6662 小时前
Python 爬虫框架设计:类封装与工程化实践
前端
广州华水科技2 小时前
潜力榜单2025年单北斗GNSS位移监测高口碑产品推荐
前端
xinyu_Jina2 小时前
OpenNana 提示词图库:多模态数据检索、分面搜索与前端性能工程
前端
暴富的Tdy2 小时前
【脚手架创建 Vue3 公共组件库】
前端·npm·npm发布
技术宅小温2 小时前
< 前端大小事: 2025年近期CSDN前端技术热点分析 >
前端
知了清语2 小时前
pkg.pr.new 快速验证第三方包-最新修复
前端
iFlow_AI2 小时前
知识驱动开发:用iFlow工作流构建本地知识库
前端·ai·rag·mcp·iflow·iflow cli·iflowcli
wordbaby2 小时前
TanStack Router 文件命名约定
前端