1. JavaScript 的单线程特性
JavaScript 是单线程的,这意味着它一次只能执行一个任务。这种设计简化了编程模型,避免了多线程环境中的复杂问题(如死锁、竞态条件等)。然而,单线程也意味着如果某个任务耗时较长,可能会阻塞后续任务的执行。
为了解决这个问题,JavaScript 引入了事件循环和非阻塞 I/O 机制,使得它能够在执行同步代码的同时,处理异步任务(如定时器、网络请求、I/O 操作等)。
2. 事件循环(Event Loop)
事件循环是 JavaScript 实现并发的核心机制。它允许 JavaScript 在执行同步代码的同时,处理异步任务。事件循环的工作流程如下:
2.1 调用栈(Call Stack)
-
调用栈用于跟踪函数的执行。每当一个函数被调用,它会被推入调用栈;当函数执行完毕,它会从调用栈中弹出。
-
JavaScript 是单线程的,因此调用栈一次只能执行一个任务。
2.2 任务队列(Task Queue)
-
异步任务(如
setTimeout
、Promise
、fetch
等)完成后,会将对应的回调函数放入任务队列中。 -
任务队列分为两种:
-
宏任务队列(MacroTask Queue):包含
setTimeout
、setInterval
、I/O 操作等。 -
微任务队列(MicroTask Queue):包含
Promise
的回调、MutationObserver
等。
-
2.3 事件循环的工作流程
-
事件循环会不断检查调用栈是否为空。
-
当调用栈为空时,事件循环会从任务队列中取出一个任务(回调函数)并推入调用栈执行。
-
在执行宏任务之前,事件循环会先清空微任务队列中的所有任务。
3. 异步任务的执行顺序
JavaScript 中的异步任务执行顺序遵循以下规则:
-
同步代码优先执行:所有同步代码会立即执行,直到调用栈为空。
-
微任务优先于宏任务:每次调用栈清空后,事件循环会先处理所有微任务,然后再处理宏任务。
-
宏任务按顺序执行:每次事件循环只会处理一个宏任务,处理完后会再次检查微任务队列。
4. 示例代码
以下代码展示了事件循环的执行顺序:
1Promise.resolve().then(() => {
输出结果:
解释:
-
同步代码
console.log("Start")
和console.log("End")
立即执行。 -
Promise
的微任务优先于setTimeout
的宏任务执行。 -
最后执行
setTimeout
的宏任务。
5. 常见的异步任务类型
5.1 宏任务(MacroTask)
-
setTimeout
、setInterval
-
I/O 操作(如文件读写、网络请求)
-
UI 渲染
5.2 微任务(MicroTask)
-
Promise
的then
、catch
、finally
-
MutationObserver
-
queueMicrotask
6. 事件循环的实际应用
理解事件循环的机制对于编写高效的异步代码至关重要。以下是一些实际应用场景:
6.1 避免阻塞主线程
通过将耗时任务(如网络请求、文件读写)放入异步任务队列,可以避免阻塞主线程,确保页面的流畅性。
6.2 优化任务调度
利用微任务优先执行的特性,可以确保高优先级的任务(如状态更新)能够及时处理。
6.3 处理复杂异步逻辑
通过 Promise
和 async/await
,可以更清晰地表达复杂的异步逻辑,避免回调地狱(Callback Hell)。
总结
JavaScript 的并发模型基于事件循环,通过调用栈、任务队列(宏任务和微任务)来管理任务的执行顺序。尽管 JavaScript 是单线程的,但事件循环和非阻塞 I/O 机制使得它能够高效地处理并发任务。