理解 JavaScript 的事件循环机制
前言
JavaScript 是一种单线程的编程语言,这意味着它一次只能执行一个任务。然而,现代 Web 应用程序需要处理大量的异步操作,如网络请求、定时器和用户交互。为了高效地管理这些异步操作,JavaScript 引入了事件循环机制。本文将深入探讨 JavaScript 的事件循环机制,并通过多个案例帮助你更好地理解这一概念。
JavaScript 线程模型
单线程模型
JavaScript 是单线程的,这意味着它一次只能执行一个任务。单线程模型的优点是避免了多线程编程中的复杂性,如竞争条件和死锁。然而,这也意味着 JavaScript 需要一种机制来处理异步操作,而不会阻塞主线程的执行。
console.log('任务1');
console.log('任务2');
console.log('任务3');
在单线程模型中,任务会按顺序执行,依次输出 任务1、任务2 和 任务3。
执行顺序
JavaScript 的执行顺序主要由以下几个部分组成:
- 调用栈(Call Stack):用于存储函数调用。
- 任务队列(Task Queue):用于存储待执行的任务。
- 事件循环(Event Loop):负责协调调用栈和任务队列之间的执行顺序。
function first() {
console.log('first');
}
function second() {
first();
console.log('second');
}
second();
console.log('third');
执行顺序为 second 调用 first,然后输出 first,接着输出 second 和 third。
调用栈和堆
调用栈是一个 LIFO(后进先出)结构,用于存储函数调用。当一个函数被调用时,它会被添加到调用栈顶部,当函数执行完毕后,它会从调用栈顶部移除。
堆是用于动态分配内存的区域,用于存储对象和函数。
function foo() {
console.log('foo');
}
function bar() {
foo();
console.log('bar');
}
bar();
在上述代码中,调用栈的执行顺序为 bar 调用 foo,然后输出 foo 和 bar。
const obj = { name: '堆内存' };
function foo() {
const bar = '栈内存';
console.log(bar);
}
foo();
对象 obj 存储在堆内存中,变量 bar 存储在栈内存中。
任务队列
任务队列是一个 FIFO(先进先出)结构,用于存储异步任务的回调函数。任务队列分为宏任务队列和微任务队列。
setTimeout(() => {
console.log('宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('微任务');
});
console.log('同步任务');
执行顺序为 同步任务,然后是 微任务,最后是 宏任务。
宏任务和微任务
- 宏任务(Macro Task):包括
setTimeout、setInterval、setImmediate、I/O 操作、UI 渲染等。 - 微任务(Micro Task):包括
Promise.then、MutationObserver、process.nextTick(Node.js)等。
setTimeout(() => {
console.log('宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('微任务');
});
console.log('同步任务');
执行顺序为 同步任务,然后是 微任务,最后是 宏任务。
同步任务和异步任务
- 同步任务:立即执行的任务,会阻塞后续任务的执行。
- 异步任务:不会立即执行的任务,会在将来的某个时间点执行,不会阻塞后续任务的执行。
console.log('同步任务');
setTimeout(() => {
console.log('异步任务');
}, 0);
执行顺序为 同步任务,然后是 异步任务。
事件循环的工作流程
事件循环的工作流程如下:
- 执行调用栈中的任务:事件循环首先会检查调用栈,如果调用栈不为空,则执行调用栈中的任务,直到调用栈为空。
- 执行微任务队列中的任务:当调用栈为空时,事件循环会检查微任务队列,并依次执行微任务队列中的所有任务,直到微任务队列为空。
- 执行宏任务队列中的任务:当微任务队列为空时,事件循环会从宏任务队列中取出一个任务,并将其添加到调用栈中执行。
- 重复上述步骤:事件循环会不断重复上述步骤,直到所有任务都被执行完毕。
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
执行顺序为 script start,script end,promise1,promise2,setTimeout。
案例分析
案例一:基本事件循环
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
console.log('script end');
执行顺序:
console.log('script start')被添加到调用栈并执行,输出script start。setTimeout被添加到宏任务队列。Promise.resolve().then被添加到微任务队列。console.log('script end')被添加到调用栈并执行,输出script end。- 调用栈为空,检查微任务队列,执行
promise1,输出promise1。 promise2被添加到微任务队列。- 执行
promise2,输出promise2。 - 微任务队列为空,检查宏任务队列,执行
setTimeout回调,输出setTimeout。
最终输出顺序为:
script start
script end
promise1
promise2
setTimeout
案例二:async 和 await
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
执行顺序:
console.log('script start')被添加到调用栈并执行,输出script start。async1被调用,输出async1 start。await async2()被执行,async2被调用,输出async2。await使得async1暂停执行,async1 end被添加到微任务队列。console.log('script end')被添加到调用栈并执行,输出script end。- 调用栈为空,检查微任务队列,执行
async1 end,输出async1 end。
最终输出顺序为:
script start
async1 start
async2
script end
async1 end
案例三:Promise 和 setTimeout
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
setTimeout(() => {
console.log('setTimeout2');
}, 0);
console.log('script end');
执行顺序:
console.log('script start')被添加到调用栈并执行,输出script start。- 第一个
setTimeout被添加到宏任务队列。 Promise.resolve().then被添加到微任务队列。- 第二个
setTimeout被添加到宏任务队列。 console.log('script end')被添加到调用栈并执行,输出script end。- 调用栈为空,检查微任务队列,执行
promise1,输出promise1。 promise2被添加到微任务队列。- 执行
promise2,输出promise2。 - 微任务队列为空,检查宏任务队列,执行第一个
setTimeout回调,输出setTimeout。 - 执行第二个
setTimeout回调,输出setTimeout2。
最终输出顺序为:
script start
script end
promise1
promise2
setTimeout
setTimeout2
案例四:综合案例
console.log('script start');
setTimeout(() => {
console.log('setTimeout1');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
setTimeout(() => {
console.log('setTimeout2');
}, 0);
console.log('script end');
执行顺序:
console.log('script start')被添加到调用栈并执行,输出script start。- 第一个
setTimeout被添加到宏任务队列。 Promise.resolve().then被添加到微任务队列。async1被调用,输出async1 start。await async2()被执行,async2被调用,输出async2。await使得async1暂停执行,async1 end被添加到微任务队列。- 第二个
setTimeout被添加到宏任务队列。 console.log('script end')被添加到调用栈并执行,输出script end。- 调用栈为空,检查微任务队列,执行
promise1,输出promise1。 promise2被添加到微任务队列。- 执行
promise2,输出promise2。 - 执行
async1 end,输出async1 end。 - 微任务队列为空,检查宏任务队列,执行第一个
setTimeout回调,输出setTimeout1。 - 执行第二个
setTimeout回调,输出setTimeout2。
最终输出顺序为:
script start
async1 start
async2
script end
promise1
promise2
async1 end
setTimeout1
setTimeout2
通过这个综合案例,我们可以看到事件循环如何协调同步任务、微任务和宏任务的执行顺序。理解这个案例将帮助你完全掌握 JavaScript 的事件循环机制。
结论
通过理解 JavaScript 的事件循环机制,我们可以更好地掌握异步编程的执行顺序和行为。事件循环协调了调用栈和任务队列之间的执行顺序,确保异步任务能够在适当的时间点执行。希望本文通过详细的解释和案例分析,能够帮助你更好地理解 JavaScript 的事件循环机制。
希望这篇文章对你有所帮助!如果有任何问题或建议,欢迎留言讨论。
找到具有 2 个许可证类型的类似代码