返回

JavaScript 事件循环机制之真谛,十一个案例剖析

前端

JavaScript 作为一门单线程语言,其代码的执行顺序由事件循环机制决定。事件循环机制是一个循环,它不断从事件队列中获取事件并执行它们。事件队列是一个先进先出的数据结构,这意味着先加入队列的事件将最先被执行。

为了更好地理解 JavaScript 的事件循环机制,我们首先需要了解以下几个概念:

  1. 主线程: JavaScript 代码的主执行线程,负责执行 JavaScript 代码。
  2. 任务队列(Task Queue): 一个先进先出的数据结构,存储需要执行的任务。
  3. 事件循环(Event Loop): 一个循环,不断从事件队列中获取事件并执行它们。
  4. 事件循环栈(Event Loop Stack): 一个先进后出的数据结构,存储当前正在执行的事件。

JavaScript 的事件循环机制可以分为以下几个步骤:

  1. 主线程执行代码。
  2. 主线程遇到一个需要等待的事件,比如 setTimeout、Promise.then 等。
  3. 主线程将该事件放入任务队列。
  4. 主线程继续执行代码,直到遇到下一个需要等待的事件。
  5. 当主线程执行完毕,它会检查任务队列中是否有事件。
  6. 如果任务队列中有事件,主线程将从任务队列中获取一个事件并将其放入事件循环栈。
  7. 主线程执行事件循环栈中的事件。
  8. 事件执行完毕后,主线程将从事件循环栈中删除该事件。
  9. 主线程重复步骤 4 到 8,直到任务队列中没有更多事件。

为了更好地理解 JavaScript 的事件循环机制,我们通过十一个案例来详细分析它的运作原理。

案例 1:setTimeout 中递归自己

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

这个案例中,我们使用 setTimeout 函数递归调用自己,每次延迟 0 毫秒。结果是,会在控制台中打印出无限个“Hello, world!”。这是因为 setTimeout 函数将每次调用的任务放入任务队列,主线程在执行完当前代码后,会从任务队列中获取任务并执行。由于每次 setTimeout 函数的延迟都是 0 毫秒,因此它们会立即被执行,导致无限循环。

案例 2:Promise.then 中递归自己

const promise = new Promise((resolve) => {
  resolve();
});

promise.then(() => {
  console.log('Hello, world!');
  promise.then(() => {
    console.log('Hello, world!');
    promise.then(() => {
      console.log('Hello, world!');
    });
  });
});

这个案例中,我们使用 Promise.then 函数递归调用自己,每次不设置延迟时间。结果是,会在控制台中打印出无限个“Hello, world!”。这是因为 Promise.then 函数将每次调用的任务放入任务队列,主线程在执行完当前代码后,会从任务队列中获取任务并执行。由于每次 Promise.then 函数的延迟都是 0 毫秒,因此它们会立即被执行,导致无限循环。

案例 3:setTimeout 与 Promise.then 结合使用

setTimeout(() => {
  console.log('Hello, world!');
  const promise = new Promise((resolve) => {
    resolve();
  });

  promise.then(() => {
    console.log('Hello, world!');
  });
}, 0);

这个案例中,我们使用 setTimeout 函数延迟执行一个 Promise。结果是,会在控制台中打印出“Hello, world!”两次。这是因为 setTimeout 函数将任务放入任务队列,主线程在执行完当前代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 0 毫秒,因此它会立即被执行,并将其任务放入任务队列。然后,主线程继续执行 Promise.then 函数,将其任务放入任务队列。当主线程执行完当前代码后,它会从任务队列中获取任务并执行。由于 Promise.then 函数的任务没有延迟,因此它会立即被执行,从而在控制台中打印出“Hello, world!”。

案例 4:同步代码与异步代码交替执行

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

这个案例中,我们交替执行同步代码和异步代码。结果是,会在控制台中打印出“Hello, world!”三次,其中同步代码打印两次,异步代码打印一次。这是因为主线程在执行完同步代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 0 毫秒,因此它会立即被执行,从而在控制台中打印出“Hello, world!”。然后,主线程继续执行同步代码,打印出“Hello, world!”。

案例 5:同步代码与异步代码嵌套执行

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

这个案例中,我们将异步代码嵌套在同步代码中执行。结果是,会在控制台中打印出“Hello, world!”五次,其中同步代码打印三次,异步代码打印两次。这是因为主线程在执行完同步代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 0 毫秒,因此它会立即被执行,从而在控制台中打印出“Hello, world!”两次。然后,主线程继续执行同步代码,打印出“Hello, world!”。

案例 6:同步代码与异步代码并发执行

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

while (true) {
  console.log('Hello, world!');
}

这个案例中,我们将同步代码与异步代码并发执行。结果是,会在控制台中打印出无限个“Hello, world!”。这是因为主线程在执行完同步代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 0 毫秒,因此它会立即被执行,从而在控制台中打印出“Hello, world!”。然后,主线程继续执行同步代码,打印出“Hello, world!”。由于同步代码是一个死循环,因此它会一直执行下去,导致无限循环。

案例 7:同步代码与异步代码混合执行

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

while (true) {
  console.log('Hello, world!');
}

这个案例中,我们将同步代码与异步代码混合执行。结果是,会在控制台中打印出无限个“Hello, world!”。这是因为主线程在执行完同步代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 0 毫秒,因此它会立即被执行,从而在控制台中打印出“Hello, world!”。然后,主线程继续执行同步代码,打印出“Hello, world!”。由于同步代码是一个死循环,因此它会一直执行下去,导致无限循环。

案例 8:同步代码与异步代码交替执行,异步代码有延迟

console.log('Hello, world!');
setTimeout(() => {
  console.log('Hello, world!');
}, 1000);
console.log('Hello, world!');

这个案例中,我们将同步代码与异步代码交替执行,异步代码有延迟。结果是,会在控制台中打印出“Hello, world!”三次,其中同步代码打印两次,异步代码打印一次。这是因为主线程在执行完同步代码后,会从任务队列中获取任务并执行。由于 setTimeout 函数的延迟是 1000 毫秒,因此它不会立即被执行,而是被放入任务队列中等待执行。然后,主线程继续执行同步代码,打印出“Hello, world!”。1000 毫秒后,setTimeout 函数的任务被执行,从而在控制台中打印出“Hello, world!”。

案例 9:同步代码与异步代码嵌套执行,异步代码有延迟

console.log('Hello, world!');
setTimeout(() => {
  console.log('Hello, world!');
  console.log('Hello, world!');
}, 1000);
console.log('Hello, world!');