ZDecode
Javascript

事件循环

浏览器中的事件循环

JavaScript 是单线程语言,只能做一件事。但是浏览器还要做很多任务(比如监听点击、执行定时器等),所以引入了 事件循环机制 来协调这些任务。

JavaScript 执行过程简述

同步任务:立即执行,放在主线程中(主调用栈)。

异步任务:交给浏览器处理(如定时器、DOM 事件、网络请求等),完成后放入任务队列,等待执行。

两类任务队列

类型描述示例
宏任务(Macro Task)主体任务,按顺序执行setTimeout、setInterval、事件绑定
微任务(Micro Task)宏任务之后立即执行的小任务,优先级更高Promise.then、MutationObserver

事件循环机制的执行步骤

  1. 执行主线程的同步任务(调用栈)。
  2. 清空所有微任务队列(Micro Task Queue)。
  3. 执行一个宏任务(Macro Task Queue)中的下一个任务。
  4. 重复步骤 2 和 3。
[主线程 Stack]

执行同步任务

[微任务队列]
→ 全部清空(比如 Promise)

[宏任务队列]
→ 执行一个(比如 setTimeout)

然后继续循环...

示例

JavaScript是单线程的,但通过事件循环机制实现了异步操作。浏览器中的事件循环主要由以下部分组成:

// 调用栈(Call Stack):同步任务在此执行
function main() {
  console.log('1');
  setTimeout(() => {
    console.log('2');
  }, 0);
  console.log('3');
}
main(); // 输出顺序:1, 3, 2

浏览器事件循环包含以下队列:

  • 微任务队列(Microtask Queue):Promise回调、MutationObserver等
  • 宏任务队列(Macrotask Queue):setTimeout、setInterval、DOM事件等
console.log('同步任务1');

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

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

console.log('同步任务2');

// 输出顺序:
// 同步任务1
// 同步任务2
// 微任务1
// 微任务2
// 宏任务1
// 宏任务2

Node.js中的事件循环

Node.js事件循环基于libuv,比浏览器更为复杂,包含多个阶段:

// Node.js事件循环的六个阶段
// 1. timers: setTimeout和setInterval回调
// 2. pending callbacks: 执行系统操作的回调,如TCP错误
// 3. idle, prepare: 内部使用
// 4. poll: 获取新的I/O事件,执行I/O相关回调
// 5. check: setImmediate回调在这执行
// 6. close callbacks: 关闭事件的回调,如socket.on('close',...)

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

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

// 在主模块中顺序不确定
// 在I/O回调中,setImmediate总是先执行

fs.readFile('文件.txt', () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
// 在I/O回调中输出顺序:immediate, timeout

浏览器与Node.js事件循环的区别

// 浏览器中:每个宏任务执行完后,会清空所有微任务
// Node.js中:每个阶段完成后才会检查微任务队列

// 浏览器中的Promise微任务优先级高于所有宏任务
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('setTimeout'), 0);
// 浏览器输出:Promise, setTimeout

// Node.js中的特殊API:process.nextTick()
// nextTick优先级高于所有微任务
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('Promise'));
// Node.js输出:nextTick, Promise

浏览器和Node.js事件循环的优缺点

浏览器优点:

// 简单直观,始终先执行所有微任务,再执行下一个宏任务
// 适合UI交互场景,确保用户操作得到及时响应
document.getElementById('button').addEventListener('click', () => {
  Promise.resolve().then(() => {
    // UI更新微任务将在任何setTimeout之前执行
    console.log('处理用户点击');
  });
});

Node.js优点:

// 分阶段执行,更精细的控制
// 适合I/O密集型应用,可有效管理资源
// process.nextTick提供优先级最高的异步操作
function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback, new TypeError('参数必须是字符串'));
  
  // 正常处理
  fs.readFile(arg, callback);
}

浏览览事件循环顺序/优先级是

// 浏览器事件循环执行顺序与优先级

// 1. 首先执行所有同步代码(主线程/调用栈中的代码)
console.log('1. 同步代码最先执行');

// 2. 调用栈清空后,检查微任务队列(microtask queue)并执行所有微任务
// 微任务包括:Promise回调、MutationObserver、queueMicrotask()等
Promise.resolve().then(() => {
  console.log('2. 微任务优先级高于宏任务');
});

// 3. 微任务队列清空后,从宏任务队列(macrotask queue)中取出一个宏任务执行
// 宏任务包括:setTimeout、setInterval、requestAnimationFrame、UI渲染等
setTimeout(() => {
  console.log('3. 宏任务在微任务之后执行');
  
  // 4. 该宏任务执行完毕后,再次检查并执行所有微任务
  Promise.resolve().then(() => {
    console.log('4. 宏任务之后会检查新产生的微任务');
  });
}, 0);

// 5. 一次事件循环:同步代码 -> 微任务队列 -> 一个宏任务 -> 微任务队列 -> ...

// 完整执行顺序示例
console.log('同步1');

setTimeout(() => {
  console.log('宏任务1');
  Promise.resolve().then(() => {
    console.log('宏任务1产生的微任务');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('微任务1');
  setTimeout(() => {
    console.log('微任务产生的宏任务');
  }, 0);
});

console.log('同步2');

// 输出顺序:
// 同步1
// 同步2
// 微任务1
// 宏任务1
// 宏任务1产生的微任务
// 微任务产生的宏任务

// 浏览器中微任务优先级:
queueMicrotask(() => console.log('queueMicrotask微任务'));
Promise.resolve().then(() => console.log('Promise微任务'));
// 二者属于同一优先级,按代码顺序执行

// 宏任务优先级(从高到低):
// 1. 用户交互事件(如点击)
// 2. requestAnimationFrame
// 3. setTimeout/setInterval

定时器的调度机制

在每一轮事件循环中,浏览器会检查:当前时间是否已经达到或超过 setTimeout 所设定的执行时间点(当前注册时间 + delay),如果是,就把它加入宏任务队列,等待执行。

// 模拟ajax
const fakeAjax = (t=1000)=> {
  return new Promise((resolve, reject) => { 
    setTimeout(()=>{
      console.log('sleep',t)
      resolve()
    },t)
  }) 
}
setTimeout(()=>{
  console.log('setTimeout 0')
},0)
fakeAjax()
setTimeout(()=>{
  console.log('setTimeout 100')
},100)