深入理解 JavaScript 事件循环(Event Loop)与异步编程机制

深入理解 JavaScript 事件循环(Event Loop)与异步编程机制

---

一、事件循环的本质是什么?

JavaScript 是单线程语言,主线程负责执行所有的任务,但它并不会阻塞异步操作。这一切都依赖于“事件循环”机制。

事件循环是浏览器和 Node.js 的核心调度器,负责将任务从任务队列(Task Queue)中调度到主线程执行。


二、宏任务 vs 微任务

JavaScript 中的任务队列分为两种:

类型 示例
宏任务(Macro Task) setTimeoutsetIntervalsetImmediate、I/O、UI渲染
微任务(Micro Task) Promise.thenMutationObserverqueueMicrotask

执行顺序:
每次事件循环:

  1. 执行主线程中的同步任务。
  2. 清空微任务队列。
  3. 执行一个宏任务。
  4. 循环重复。

三、代码深入分析

来看一个常见例子:

console.log('1');

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

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

console.log('4');

输出结果:

1
4
3
2

分析步骤:

  1. 同步执行 console.log('1')
  2. 注册一个 setTimeout 宏任务
  3. 注册一个 Promise.then 微任务
  4. 执行 console.log('4')
  5. 当前调用栈空,立即执行微任务 console.log('3')
  6. 然后进入下一轮事件循环,执行宏任务 console.log('2')

四、真实案例分析:异步加载优化

示例:异步加载多个模块但按顺序执行

async function loadModulesInOrder() {
    
    
  await importModule('module1.js');
  await importModule('module2.js');
  await importModule('module3.js');
}

function importModule(url) {
    
    
  return new Promise((resolve) => {
    
    
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    document.body.appendChild(script);
  });
}
  • 利用了 Promiseawait 的微任务机制
  • 每次 await 后会将剩余代码推入微任务队列,确保模块顺序执行

五、Node.js 中的事件循环

Node 的事件循环与浏览器略有不同,它的阶段包括:

  1. timers(执行 setTimeout/setInterval 回调)
  2. pending callbacks
  3. idle, prepare
  4. poll(I/O 阶段)
  5. check(执行 setImmediate
  6. close callbacks
  7. 微任务(每个阶段之后处理)

示例对比:

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

setImmediate(() => {
    
    
  console.log("immediate");
});

在 Node.js 中,这两者谁先执行并不确定,依赖于当前 poll 阶段是否为空。


六、异步控制最佳实践

1. 并发控制

async function limitConcurrency(tasks, limit = 5) {
    
    
  const executing = new Set();

  const run = async (task) => {
    
    
    const p = task();
    executing.add(p);
    p.finally(() => executing.delete(p));
    if (executing.size >= limit) {
    
    
      await Promise.race(executing);
    }
    return p;
  };

  for (const task of tasks) {
    
    
    await run(task);
  }
}

2. 防抖和节流(高频事件优化)

function debounce(fn, delay) {
    
    
  let timer = null;
  return function (...args) {
    
    
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

function throttle(fn, delay) {
    
    
  let lastTime = 0;
  return function (...args) {
    
    
    const now = Date.now();
    if (now - lastTime >= delay) {
    
    
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

七、面试高频题实战模拟

console.log('start');

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

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

console.log('end');

输出:

start
end
promise1
promise2
setTimeout

考点:

  • 同步任务优先
  • 多级 then 都是微任务
  • 宏任务最后执行

八、总结

关键点 描述
事件循环 控制任务调度,单线程并发关键
微任务优先 微任务执行在每轮宏任务之后、下一轮宏任务之前
setTimeout ≠ 立即执行 即便 delay 为 0 也会延迟一轮事件循环
实践应用 并发控制、模块加载、性能优化

延伸阅读建议