JavaScript 事件循环机制之真谛,十一个案例剖析
2023-10-27 18:20:05
JavaScript 作为一门单线程语言,其代码的执行顺序由事件循环机制决定。事件循环机制是一个循环,它不断从事件队列中获取事件并执行它们。事件队列是一个先进先出的数据结构,这意味着先加入队列的事件将最先被执行。
为了更好地理解 JavaScript 的事件循环机制,我们首先需要了解以下几个概念:
- 主线程: JavaScript 代码的主执行线程,负责执行 JavaScript 代码。
- 任务队列(Task Queue): 一个先进先出的数据结构,存储需要执行的任务。
- 事件循环(Event Loop): 一个循环,不断从事件队列中获取事件并执行它们。
- 事件循环栈(Event Loop Stack): 一个先进后出的数据结构,存储当前正在执行的事件。
JavaScript 的事件循环机制可以分为以下几个步骤:
- 主线程执行代码。
- 主线程遇到一个需要等待的事件,比如 setTimeout、Promise.then 等。
- 主线程将该事件放入任务队列。
- 主线程继续执行代码,直到遇到下一个需要等待的事件。
- 当主线程执行完毕,它会检查任务队列中是否有事件。
- 如果任务队列中有事件,主线程将从任务队列中获取一个事件并将其放入事件循环栈。
- 主线程执行事件循环栈中的事件。
- 事件执行完毕后,主线程将从事件循环栈中删除该事件。
- 主线程重复步骤 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!');