16 多线程
loyalvi Lv7

13 多线程

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
 console.log("Start of script");
// 宏任务:setTimeout
setTimeout(() => {
console.log("setTimeout 1");
}, 0);
// 微任务:Promise
Promise.resolve().then(() => {
console.log("Promise 1");
});
// 宏任务:setInterval
setInterval(() => {
console.log("setInterval 1");
}, 1000);
// 微任务:queueMicrotask
queueMicrotask(() => {
console.log("queueMicrotask 1");
});
//用于在浏览器重绘之前执行回调
requestAnimationFrame(() => {
console.log("requestAnimationFrame callback");
});
// 宏任务:setTimeout
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
// 微任务:Promise
Promise.resolve().then(() => {
console.log("Promise 2");
});
console.log("End of script");
// 事件监听器
document.getElementById("myButton").addEventListener("click", () => {
console.log("Button clicked");
});

console:

1
2
3
4
5
6
7
8
9
10
11
Start of script
End of script
Promise 1
queueMicrotask 1
Promise 2
requestAnimationFrame callback
setTimeout 1
setTimeout 2
setInterval 1
Button clicked
setInterval 1

事件循环

JavaScript 的事件循环(Event Loop)是理解 JavaScript 异步编程和运行机制的关键概念。它解释了 JavaScript 如何在单线程环境下实现异步操作,如定时器、回调函数、Promise 等。以下是 JavaScript 事件循环的详细解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart TD
A[事件循环开始] --> B{调用栈为空?}
B -->|是| C{微任务队列有任务?}
B -->|否| H[执行调用栈任务]
C -->|是| D[取出微任务执行]
D --> E{微任务队列为空?}
E -->|否| C
E -->|是| F{调用栈为空?}
F -->|是| G{微任务队列为空?}
G -->|是| I[从任务队列取出任务执行]
G -->|否| C
F -->|否| H
I --> A
H --> A

1. JavaScript 的运行机制

JavaScript 是单线程语言,这意味着它在同一时间只能执行一个任务。然而,JavaScript 通过事件循环机制实现了异步操作,使得代码可以“非阻塞”地运行。

核心概念:

  • 调用栈(Call Stack):记录当前正在执行的函数。
  • 任务队列(Task Queue):存放待执行的回调函数。
  • 微任务队列(Microtask Queue):存放由 Promise、MutationObserver 等产生的回调。
  • 事件循环(Event Loop):监控调用栈和任务队列,当调用栈为空时,从任务队列中取出任务执行。

2. 事件循环的工作原理

事件循环的主要工作是协调调用栈、任务队列和微任务队列之间的关系。

2.1 调用栈

调用栈是一个后进先出(LIFO)的栈结构,记录当前正在执行的函数。每当一个函数被调用时,它会被压入调用栈;当函数执行完成时,它会被弹出。

2.2 任务队列

任务队列存放由异步操作(如 setTimeoutsetIntervaladdEventListener 等)产生的回调函数。这些回调函数会在调用栈为空时被事件循环取出并执行。

2.3 微任务队列

微任务队列存放由 Promise、MutationObserver 等产生的回调函数。微任务的优先级高于任务队列中的任务,会在当前调用栈清空后立即执行。

2.4 事件循环

事件循环的主要工作是:

  1. 检查调用栈是否为空:如果调用栈为空,事件循环会从微任务队列中取出任务执行,直到微任务队列为空。
  2. 执行微任务:微任务执行完成后,事件循环会检查调用栈是否为空。
  3. 执行任务队列中的任务:如果调用栈为空且微任务队列为空,事件循环会从任务队列中取出任务执行。

3. 示例解析

示例 1:任务队列和调用栈

1
2
3
4
5
6
7
8
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');

执行顺序
4. console.log('Start') 执行,输出 Start
5. 两个 setTimeout 被添加到任务队列中。
6. console.log('End') 执行,输出 End
7. 调用栈为空,事件循环从任务队列中取出第一个回调函数执行,输出 Timeout 1
8. 事件循环从任务队列中取出第二个回调函数执行,输出 Timeout 2
输出结果

1
2
3
4
Start
End
Timeout 1
Timeout 2

示例 2:微任务队列和任务队列

1
2
3
4
5
6
7
8
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');

执行顺序
9. console.log('Start') 执行,输出 Start
10. setTimeout 被添加到任务队列中。
11. Promise.resolve().then() 被添加到微任务队列中。
12. console.log('End') 执行,输出 End
13. 调用栈为空,事件循环检查微任务队列,执行 Promise 回调,输出 Promise
14. 微任务队列为空,事件循环检查任务队列,执行 setTimeout 回调,输出 Timeout
输出结果

1
2
3
4
Start
End
Promise
Timeout

4. 事件循环的阶段

事件循环分为多个阶段,每个阶段都有特定的任务:
15. 宏任务(Macrotask)
- 包括 setTimeoutsetIntervalsetImmediate(Node.js)、I/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
16. 微任务(Microtask)
- 包括 PromiseMutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。


5. Node.js 与浏览器中的事件循环

虽然浏览器和 Node.js 的事件循环机制类似,但它们在实现上有一些差异:

  • 浏览器:事件循环由浏览器引擎(如 V8)管理。
  • Node.js:事件循环由 libuv 库实现,支持更多类型的异步操作(如文件系统操作、网络请求等)。

6. 总结

JavaScript 的事件循环是理解异步编程的关键。它通过协调调用栈、任务队列和微任务队列,实现了高效的异步操作。事件循环的工作机制确保了 JavaScript 即使在单线程环境下也能高效地处理异步任务。
掌握事件循环的原理有助于更好地理解 JavaScript 中的异步编程,避免常见的错误(如回调地狱、Promise 陷阱等)。

settimeout能不能精准计时

setTimeout 是 JavaScript 中用于实现延迟执行的函数,但它并不能保证精准计时,主要有以下几方面原因:
一、事件循环机制的影响
17. JavaScript 是单线程运行的,采用事件驱动机制。当调用 setTimeout 设置延迟任务时,这个任务会被放入事件队列中。例如:

1
2
3
setTimeout(() => {
console.log('延迟任务');
}, 1000);

这段代码的意图是让“延迟任务”在1000毫秒后执行。但是,如果在 setTimeout 调用之后,主线程上还有其他耗时任务在执行,那么事件循环就需要等待这个耗时任务完成,才能从事件队列中取出 setTimeout 的回调函数来执行。比如:
1
2
3
4
5
6
setTimeout(() => {
console.log('延迟任务');
}, 1000);
// 假设这是一个耗时2000毫秒的同步任务
for (let i = 0; i < 1000000000; i++) {}
console.log('同步任务完成');

在这种情况下,“延迟任务”实际上会在2000毫秒之后的某个时刻才执行,因为事件循环需要等待同步任务完成。
二、浏览器或JavaScript引擎的限制

  1. 浏览器或 JavaScript 引擎对 setTimeout 的最小延迟时间有一定的限制。在大多数浏览器中,setTimeout 的最小延迟时间是4毫秒(在某些情况下,如页面处于后台标签页时,这个时间可能会更长)。即使你设置的延迟时间小于4毫秒,实际的延迟也会被调整为至少4毫秒。例如:
    1
    2
    3
    setTimeout(() => {
    console.log('快速延迟任务');
    }, 1); // 实际延迟至少为4毫秒
    这种限制是为了防止页面被过于频繁的定时任务所阻塞,从而影响用户体验。
    三、系统时间调整的影响(较少见)
  2. 如果在 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,则新的回调会在下一次重绘之前执行。

执行顺序

  1. 宏任务:当前帧的宏任务执行完毕。
  2. 微任务:当前帧的微任务队列中的所有任务执行完毕。
  3. requestAnimationFrame:当前帧的 requestAnimationFrame 回调执行。
  4. 重绘:浏览器进行重绘。
  5. 下一个宏任务:进入事件循环的下一个宏任务。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
console.log('1. Start');
setTimeout(() => {
console.log('2. Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise');
});
requestAnimationFrame(() => {
console.log('4. First rAF');
});
requestAnimationFrame(() => {
requestAnimationFrame(() => {
console.log('5. Second rAF');
});
});
console.log('6. End');

执行顺序将是:

1
2
3
4
5
6
8. Start
9. End
10. Promise
11. First rAF
12. Timeout
13. Second rAF

总结

  • 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)
- 包括 setTimeoutsetIntervalrequestAnimationFrameI/O 操作等。
- 每次执行一个宏任务后,事件循环会检查微任务队列。
15. 微任务(Microtask)
- 包括 PromiseMutationObserver 等。
- 微任务的优先级高于宏任务,会在每次宏任务执行后立即执行。

2.2 Node.js 的事件循环阶段

Node.js 的事件循环由 libuv 库实现,分为以下阶段:
16. Timers
- 执行到期的 setTimeoutsetInterval 回调。
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. 定时器的实现

  • 浏览器
    • setTimeoutsetInterval 是由浏览器的定时器机制实现的。
    • 它们依赖于浏览器的事件循环,但具体实现细节由浏览器引擎决定。
  • Node.js
    • setTimeoutsetInterval 是由 libuv 的定时器机制实现的。
    • 它们的行为与浏览器类似,但 Node.js 提供了更细粒度的控制(如 setImmediate)。

3.1 setImmediate

Node.js 提供了 setImmediate,它在事件循环的 Check 阶段执行回调。这与浏览器中的 setTimeout 有所不同:

1
2
3
4
5
6
setTimeout(() => {
console.log('Timeout');
}, 0);
setImmediate(() => {
console.log('Immediate');
});

在 Node.js 中,setImmediate 的回调会在 Check 阶段执行,而 setTimeout 的回调会在 Timers 阶段执行。因此,setImmediate 的回调通常会在 setTimeout 的回调之后执行。

4. I/O 操作

  • 浏览器
    • I/O 操作主要与网络请求(如 fetchXMLHttpRequest)和文件系统(通过 Web APIs)相关。
    • 浏览器的 I/O 操作通常由浏览器内核实现,与 JavaScript 运行时紧密集成。
  • 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.nextTicksetImmediate 等 API 控制事件循环的行为。

5.1 process.nextTick

Node.js 提供了 process.nextTick,它允许开发者在当前操作完成后立即执行回调:

1
2
3
4
5
console.log('Start');
process.nextTick(() => {
console.log('Next Tick');
});
console.log('End');

输出:
复制

1
2
3
Start
End
Next Tick

process.nextTick 的回调会在当前操作完成后立即执行,优先级高于微任务和宏任务。

6. 总结

Node.js 和浏览器中的事件循环虽然在核心机制上类似,但在实现和具体行为上存在以下主要区别:
22. 运行环境
- 浏览器侧重于用户交互和页面渲染。
- Node.js 侧重于 I/O 操作和网络通信。
23. 事件循环阶段
- 浏览器的事件循环分为宏任务和微任务。
- Node.js 的事件循环分为多个阶段(如 Timers、Poll、Check 等)。
24. 定时器实现
- 浏览器使用 setTimeoutsetInterval
- Node.js 提供了 setTimeoutsetIntervalsetImmediate
25. I/O 操作
- 浏览器的 I/O 操作主要与网络和文件系统相关。
- Node.js 的 I/O 操作由 libuv 库实现,支持丰富的异步操作。
26. 事件循环控制
- 浏览器的事件循环由浏览器引擎管理,开发者无法直接控制。
- Node.js 提供了 process.nextTick 等 API,允许开发者更细粒度地控制事件循环。
理解这些差异有助于更好地编写高效、可靠的代码,尤其是在处理异步操作和性能优化时。

由 Hexo 驱动 & 主题 Keep
访客数 访问量