Event

EventLoop

事件循环(Event Loop)是现代编程中一个核心的概念,尤其在异步编程中起着至关重要的作用。它在浏览器、Node.js 和其他基于异步 I/O 的环境中广泛使用。本教程将带你从基础到深入理解事件循环。


  1. 什么是事件循环?

事件循环是一种调度机制,用来管理程序的执行顺序。它使得非阻塞 I/O 和异步任务得以实现,通过以下步骤工作:

  1. 检查是否有任务需要处理。
  2. 依次执行任务。
  3. 等待新任务并重复。

事件循环是单线程的,但它能通过异步任务模拟多任务运行。


  1. 事件循环的工作模型

以下是事件循环的几个关键组成部分:

任务队列(Task Queue)

存放需要执行的任务。任务可以分为:

  • ​宏任务(​​Macro​​​ Task)​:主程序、setTimeoutsetIntervalI/O
  • ​微任务(Micro Task)​:Promise.thenqueueMicrotask

事件循环的步骤

  1. 执行一个宏任务(如主线程代码)。
  2. 检查微任务队列,执行所有微任务。
  3. 如果有渲染更新,则进行渲染。
  4. 取出下一个宏任务,重复上述过程。

  1. ​事件循环的代码实例(​​​JavaScript​)

以下是一个例子,演示任务队列的顺序:

console.log("开始");
// 宏任务
setTimeout(() => {
  console.log("setTimeout");
}, 0);
// 微任务
Promise.resolve().then(() => {
  console.log("Promise.then");
});
console.log("结束");

输出顺序:

开始
结束
Promise.then
setTimeout

解析:

  1. 主线程首先执行同步代码。
  2. setTimeout的回调被放入宏任务队列。
  3. Promise.then的回调被放入微任务队列。
  4. 主线程代码执行完后,先执行微任务,再执行宏任务。

  1. 在浏览器环境中的事件循环

浏览器的事件循环还涉及到渲染过程:

  • 每次事件循环的末尾会检查是否需要重新渲染。
  • 渲染通常在 16ms(即 60fps)内完成。

示例

setTimeout(() => {
  console.log("setTimeout");
}, 0);
requestAnimationFrame(() => {
  console.log("requestAnimationFrame");
});
Promise.resolve().then(() => {
  console.log("Promise");
});

输出顺序:

Promise
requestAnimationFrame
setTimeout

  1. 在 Node.js 中的事件循环

Node.js 的事件循环分为六个阶段:

  1. ​timers​:执行 setTimeoutsetInterval 的回调。
  2. ​I/O callbacks​:处理一些系统操作的回调,如文件 I/O。
  3. ​idle, prepare​:仅供内部使用。
  4. ​poll​:检索新的 I/O 事件。
  5. ​check​:执行 setImmediate 的回调。
  6. ​close callbacks​:执行 close 事件的回调。

示例

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

输出顺序:

setTimeout
setImmediate

setImmediate
setTimeout

解析:

  • 顺序可能取决于事件循环进入 timers 还是 check 阶段。

  1. 应用场景
  • ​非阻塞 I/O​:处理网络请求、文件操作。
  • ​动画更新​:在浏览器中,通过 requestAnimationFrame 和微任务队列。
  • ​任务调度​:使用 setTimeoutPromise 来控制代码执行的先后顺序。

  1. 进阶:自定义事件循环

你可以通过代码实现一个简单的事件循环:

class EventLoop {
  constructor() {
    this.queue = [];
  }
  add(task) {
    this.queue.push(task);
  }
  run() {
    while (this.queue.length > 0) {
      const task = this.queue.shift();
      task();
    }
  }
}
const loop = new EventLoop();
loop.add(() => console.log("Task 1"));
loop.add(() => console.log("Task 2"));
loop.run();

输出:

Task 1
Task 2

通过以上内容,你应该对事件循环有了一个全面的了解。如果你希望更深入某部分(如浏览器 vs Node.js 或异步编程模式),可以进一步交流!

EventTarget 接口

事件的本质是程序各个组成部分之间的一种通信方式,也是异步编程的一种实现。DOM 支持大量的事件,本章开始介绍 DOM 的事件编程。

介绍具体的事件之前,先来看看如何让 DOM 节点监听事件。

DOM节点的事件操作

DOM 节点的事件操作(监听和触发),都定义在EventTarget接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequestAudioNodeAudioContext)也部署了这个接口。

该接口主要提供三个实例方法。

  • addEventListener():绑定事件的监听函数
  • removeEventListener():移除事件的监听函数
  • dispatchEvent():触发事件

EventTarget.addEventListener()

用于在当前节点或对象上(即部署了 EventTarget 接口的对象),定义一个特定事件的监听函数。一旦这个事件发生,就会执行监听函数。该方法没有返回值。

target.addEventListener(type, listener[, useCapture]);

该方法接受三个参数。

  • type:事件名称,大小写敏感。
  • listener:监听函数。事件发生时,会调用该监听函数。
  • useCapture:布尔值,如果设为true,表示监听函数将在捕获阶段(capture)触发(参见后文《事件的传播》部分)。该参数可选,默认值为false(监听函数只在冒泡阶段被触发)。