13 多线程
例子
1 | console.log("Start of script"); |
console:
1 | Start of script |
事件循环
JavaScript 的事件循环(Event Loop)是理解 JavaScript 异步编程和运行机制的关键概念。它解释了 JavaScript 如何在单线程环境下实现异步操作,如定时器、回调函数、Promise 等。以下是 JavaScript 事件循环的详细解析。
1 | flowchart TD |
1. JavaScript 的运行机制
JavaScript 是单线程语言,这意味着它在同一时间只能执行一个任务。然而,JavaScript 通过事件循环机制实现了异步操作,使得代码可以“非阻塞”地运行。
核心概念:
- 调用栈(Call Stack):记录当前正在执行的函数。
- 任务队列(Task Queue):存放待执行的回调函数。
- 微任务队列(Microtask Queue):存放由 Promise、
MutationObserver等产生的回调。 - 事件循环(Event Loop):监控调用栈和任务队列,当调用栈为空时,从任务队列中取出任务执行。
2. 事件循环的工作原理
事件循环的主要工作是协调调用栈、任务队列和微任务队列之间的关系。
2.1 调用栈
调用栈是一个后进先出(LIFO)的栈结构,记录当前正在执行的函数。每当一个函数被调用时,它会被压入调用栈;当函数执行完成时,它会被弹出。
2.2 任务队列
任务队列存放由异步操作(如 setTimeout、setInterval、addEventListener 等)产生的回调函数。这些回调函数会在调用栈为空时被事件循环取出并执行。
2.3 微任务队列
微任务队列存放由 Promise、MutationObserver 等产生的回调函数。微任务的优先级高于任务队列中的任务,会在当前调用栈清空后立即执行。
2.4 事件循环
事件循环的主要工作是:
- 检查调用栈是否为空:如果调用栈为空,事件循环会从微任务队列中取出任务执行,直到微任务队列为空。
- 执行微任务:微任务执行完成后,事件循环会检查调用栈是否为空。
- 执行任务队列中的任务:如果调用栈为空且微任务队列为空,事件循环会从任务队列中取出任务执行。
3. 示例解析
示例 1:任务队列和调用栈
1 | console.log('Start'); |
执行顺序:
4. console.log('Start') 执行,输出 Start。
5. 两个 setTimeout 被添加到任务队列中。
6. console.log('End') 执行,输出 End。
7. 调用栈为空,事件循环从任务队列中取出第一个回调函数执行,输出 Timeout 1。
8. 事件循环从任务队列中取出第二个回调函数执行,输出 Timeout 2。
输出结果:
1 | Start |
示例 2:微任务队列和任务队列
1 | console.log('Start'); |
执行顺序:
9. console.log('Start') 执行,输出 Start。
10. setTimeout 被添加到任务队列中。
11. Promise.resolve().then() 被添加到微任务队列中。
12. console.log('End') 执行,输出 End。
13. 调用栈为空,事件循环检查微任务队列,执行 Promise 回调,输出 Promise。
14. 微任务队列为空,事件循环检查任务队列,执行 setTimeout 回调,输出 Timeout。
输出结果:
1 | Start |
4. 事件循环的阶段
事件循环分为多个阶段,每个阶段都有特定的任务:
15. 宏任务(Macrotask):
- 包括 setTimeout、setInterval、setImmediate(Node.js)、I/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
16. 微任务(Microtask):
- 包括 Promise、MutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。
5. Node.js 与浏览器中的事件循环
虽然浏览器和 Node.js 的事件循环机制类似,但它们在实现上有一些差异:
- 浏览器:事件循环由浏览器引擎(如 V8)管理。
- Node.js:事件循环由 libuv 库实现,支持更多类型的异步操作(如文件系统操作、网络请求等)。
6. 总结
JavaScript 的事件循环是理解异步编程的关键。它通过协调调用栈、任务队列和微任务队列,实现了高效的异步操作。事件循环的工作机制确保了 JavaScript 即使在单线程环境下也能高效地处理异步任务。
掌握事件循环的原理有助于更好地理解 JavaScript 中的异步编程,避免常见的错误(如回调地狱、Promise 陷阱等)。
settimeout能不能精准计时
setTimeout 是 JavaScript 中用于实现延迟执行的函数,但它并不能保证精准计时,主要有以下几方面原因:
一、事件循环机制的影响
17. JavaScript 是单线程运行的,采用事件驱动机制。当调用 setTimeout 设置延迟任务时,这个任务会被放入事件队列中。例如:
1
2
3setTimeout(() => {
console.log('延迟任务');
}, 1000);
这段代码的意图是让“延迟任务”在1000毫秒后执行。但是,如果在 setTimeout 调用之后,主线程上还有其他耗时任务在执行,那么事件循环就需要等待这个耗时任务完成,才能从事件队列中取出 setTimeout 的回调函数来执行。比如:
1
2
3
4
5
6setTimeout(() => {
console.log('延迟任务');
}, 1000);
// 假设这是一个耗时2000毫秒的同步任务
for (let i = 0; i < 1000000000; i++) {}
console.log('同步任务完成');
在这种情况下,“延迟任务”实际上会在2000毫秒之后的某个时刻才执行,因为事件循环需要等待同步任务完成。
二、浏览器或JavaScript引擎的限制
- 浏览器或 JavaScript 引擎对
setTimeout的最小延迟时间有一定的限制。在大多数浏览器中,setTimeout的最小延迟时间是4毫秒(在某些情况下,如页面处于后台标签页时,这个时间可能会更长)。即使你设置的延迟时间小于4毫秒,实际的延迟也会被调整为至少4毫秒。例如:这种限制是为了防止页面被过于频繁的定时任务所阻塞,从而影响用户体验。1
2
3setTimeout(() => {
console.log('快速延迟任务');
}, 1); // 实际延迟至少为4毫秒
三、系统时间调整的影响(较少见) - 如果在
setTimeout执行期间,系统时间被人为调整(比如将系统时间向前拨动),也可能会对setTimeout的执行产生影响。不过这种情况在实际开发中比较少见,主要是在一些特殊场景下(如服务器时间校准等)可能会出现。
虽然setTimeout不能保证精准计时,但在大多数情况下,它能够满足一般的延迟执行需求。如果需要更精准的计时,可以考虑使用setInterval(虽然它也有类似的限制)或者 Web Workers(在后台线程中执行定时任务,减少主线程的影响)等其他方法,具体选择取决于应用场景和精度要求。
requestAnimationFrame
requestAnimationFrame 既不是传统的宏任务也不是微任务,它有自己独特的调度机制。不过,为了更好地理解它的执行时机,我们可以将其与宏任务和微任务进行对比。
宏任务(Macrotask)
宏任务包括整体的 script(整体代码)、setTimeout、setInterval、setImmediate(Node.js 环境)、I/O 操作、UI 交互事件等。宏任务的特点是它们会在当前任务执行完毕后,进入事件循环的下一个阶段进行处理。
微任务(Microtask)
微任务包括 Promise、MutationObserver(用于监听 DOM 变化)、process.nextTick(Node.js 环境)等。微任务的特点是它们会在当前任务执行完毕后,立即执行,不会进入事件循环的下一个阶段。
requestAnimationFrame
requestAnimationFrame 用于在下一次重绘之前执行回调函数。它的执行时机在宏任务和微任务之间。具体来说,requestAnimationFrame 的回调会在当前帧的所有宏任务执行完毕后,且在下一次重绘之前执行。如果在 requestAnimationFrame 的回调中再次调用 requestAnimationFrame,则新的回调会在下一次重绘之前执行。
执行顺序
- 宏任务:当前帧的宏任务执行完毕。
- 微任务:当前帧的微任务队列中的所有任务执行完毕。
requestAnimationFrame:当前帧的requestAnimationFrame回调执行。- 重绘:浏览器进行重绘。
- 下一个宏任务:进入事件循环的下一个宏任务。
示例
1 | console.log('1. Start'); |
执行顺序将是:
1 | 8. Start |
总结
requestAnimationFrame:在当前帧的所有宏任务和微任务执行完毕后,且在下一次重绘之前执行。- 微任务:在当前任务执行完毕后立即执行,不会进入事件循环的下一个阶段。
- 宏任务:在当前任务执行完毕后,进入事件循环的下一个阶段执行。
requestAnimationFrame的这种独特的调度机制使其非常适合用于动画渲染,因为它能够确保在浏览器重绘之前完成必要的更新,从而提高动画的流畅性。
node
Node.js 和浏览器中的 JavaScript 都使用事件循环来处理异步任务,但它们在实现和具体行为上存在一些关键差异。这些差异主要源于它们运行环境的不同:浏览器侧重于用户交互和页面渲染,而 Node.js 侧重于服务器端的 I/O 操作和网络通信。
以下是 Node.js 事件循环和浏览器事件循环的主要区别:
1. 运行环境
- 浏览器:
- 浏览器的事件循环由浏览器引擎(如 V8、SpiderMonkey 等)管理。
- 主要处理用户交互(如点击、滚动)、页面渲染、定时器、网络请求等任务。
- Node.js:
- Node.js 的事件循环由底层的 libuv 库实现。
- 主要处理 I/O 操作(如文件读写、网络请求)、定时器、进程间通信等任务。
2. 事件循环的阶段
Node.js 和浏览器的事件循环都包含多个阶段,但具体阶段的划分和功能有所不同。
2.1 浏览器的事件循环阶段
浏览器的事件循环主要分为:
14. 宏任务(Macrotask):
- 包括 setTimeout、setInterval、requestAnimationFrame、I/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
15. 微任务(Microtask):
- 包括 Promise、MutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。
2.2 Node.js 的事件循环阶段
Node.js 的事件循环由 libuv 库实现,分为以下阶段:
16. Timers:
- 执行到期的 setTimeout 和 setInterval 回调。
17. Pending Callbacks:
- 执行某些系统操作的回调(如 TCP 错误回调)。
18. Idle, Prepare:
- 内部使用,通常与 Node.js 的 C++ 插件相关。
19. Poll:
- 执行 I/O 回调(如文件读写、网络请求)。
- 如果没有待处理的 I/O 回调,事件循环可能会在此阶段阻塞,等待新的事件。
20. Check:
- 执行 setImmediate 的回调。
21. Close Callbacks:
- 执行某些资源关闭的回调(如 socket.close)。
3. 定时器的实现
- 浏览器:
setTimeout和setInterval是由浏览器的定时器机制实现的。- 它们依赖于浏览器的事件循环,但具体实现细节由浏览器引擎决定。
- Node.js:
setTimeout和setInterval是由 libuv 的定时器机制实现的。- 它们的行为与浏览器类似,但 Node.js 提供了更细粒度的控制(如
setImmediate)。
3.1 setImmediate
Node.js 提供了 setImmediate,它在事件循环的 Check 阶段执行回调。这与浏览器中的 setTimeout 有所不同:
1 | setTimeout(() => { |
在 Node.js 中,setImmediate 的回调会在 Check 阶段执行,而 setTimeout 的回调会在 Timers 阶段执行。因此,setImmediate 的回调通常会在 setTimeout 的回调之后执行。
4. I/O 操作
- 浏览器:
- I/O 操作主要与网络请求(如
fetch、XMLHttpRequest)和文件系统(通过 Web APIs)相关。 - 浏览器的 I/O 操作通常由浏览器内核实现,与 JavaScript 运行时紧密集成。
- I/O 操作主要与网络请求(如
- Node.js:
- I/O 操作是 Node.js 的核心功能之一,由 libuv 库实现。
- Node.js 提供了丰富的 I/O API(如
fs模块、http模块),支持非阻塞 I/O 操作。 - Node.js 的 I/O 操作回调通常在
Poll阶段执行。
5. 事件循环的控制
- 浏览器:
- 事件循环由浏览器引擎管理,开发者无法直接控制事件循环的行为。
- 事件循环的行为主要受 JavaScript 代码和浏览器内核的限制。
- Node.js:
- 事件循环由 libuv 库实现,提供了更细粒度的控制。
- 开发者可以通过
process.nextTick、setImmediate等 API 控制事件循环的行为。
5.1 process.nextTick
Node.js 提供了 process.nextTick,它允许开发者在当前操作完成后立即执行回调:
1 | console.log('Start'); |
输出:
复制
1 | Start |
process.nextTick 的回调会在当前操作完成后立即执行,优先级高于微任务和宏任务。
6. 总结
Node.js 和浏览器中的事件循环虽然在核心机制上类似,但在实现和具体行为上存在以下主要区别:
22. 运行环境:
- 浏览器侧重于用户交互和页面渲染。
- Node.js 侧重于 I/O 操作和网络通信。
23. 事件循环阶段:
- 浏览器的事件循环分为宏任务和微任务。
- Node.js 的事件循环分为多个阶段(如 Timers、Poll、Check 等)。
24. 定时器实现:
- 浏览器使用 setTimeout 和 setInterval。
- Node.js 提供了 setTimeout、setInterval 和 setImmediate。
25. I/O 操作:
- 浏览器的 I/O 操作主要与网络和文件系统相关。
- Node.js 的 I/O 操作由 libuv 库实现,支持丰富的异步操作。
26. 事件循环控制:
- 浏览器的事件循环由浏览器引擎管理,开发者无法直接控制。
- Node.js 提供了 process.nextTick 等 API,允许开发者更细粒度地控制事件循环。
理解这些差异有助于更好地编写高效、可靠的代码,尤其是在处理异步操作和性能优化时。