JS 执行机制
Last Edited Time
Feb 11, 2022 07:58 AM
date
Sep 8, 2021
slug
js-event-loop
status
Published
tags
JavaScript
个人笔记
必读系列
summary
- NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的
type
Post
目录
浏览器和 NodeJS 执行机制
浏览器执行机制图
NodeJS 执行机制图
阶段概述:
每个框被称为事件循环机制的一个阶段, 每个阶段都有一个 FIFO 队列来执行回调
- 定时器:本阶段执行已经被
setTimeout()
和setInterval()
的调度回调函数。
- 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
- idle, prepare:仅系统内部使用。
- 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和
setImmediate()
调度的之外),其余情况 node 将在适当的时候在此阻塞。
- 检测:
setImmediate()
回调函数在这里执行。
- 关闭的回调函数:一些关闭的回调函数,如:
socket.on('close', ...)
。
macroTaskQueue/microTaskQueue/nextTickQueue 执行顺序
浏览器环境
setImmediate 不是浏览器标准功能, 兼容性不好
- 宏任务: setTimeout, setInterval, I/O,setImmediate(如果存在, 目前只有 Edge 和 IE11 支持),requestAnimationFrame(存在争议)
- 微任务: Promises,MutationObserver
总结:
- 浏览器环境下, 会依次执行
宏任务 -> 微任务栈 -> 宏任务 -> 微任务栈
NodeJS 环境
NodeJS 有主执行线程, I/O线程, worker_threads 线程, 没有定时器线程, 也没有宏任务栈的概念, 但是在 NodeJS 11 的某个版本之后, 主动向浏览器的逻辑靠拢, 导致可以近似理解为 NodeJS 是有宏任务栈的
- 阶段任务:setTimeout, setInterval, setImmediate
- 微任务:process.nextTick, promise.then
总结:
- NodeJS 11 的某个版本之前: 在每个
timmer
和immediate
执行之后, 并不会执行nextTick栈 → 微任务栈
, 而是会在每个阶段结束前统一清空
- NodeJS 11 的某个版本之后: 在每个
timmer
和immediate
执行之后, 都会执行nextTick栈 → 微任务栈
, 为了与浏览器事件机制保持一致
复现代码
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(function() {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(function() {
console.log('promise2');
});
}, 0);
// 这里加长主任务运行时间, 保证 setTimeout 回调时间已到
let old = Date.now();
while (Date.now() - old <= 1000) {}
// 在浏览器环境和 NodeJS 11 的某个版本之后的执行结果为 timer1, promise1, timer2, promise2
// 在 NodeJS 11 的某个版本之前, 结果为 timer1, timer2, promise1, promise2
相关 Issue:
- timers: run nextTicks after each immediate and timer #22842.
- MacroTask and MicroTask execution order #22257.
- implement queueMicrotask #22951.
宏任务微任务兼容
微任务基本原理
核心微任务代码实现见
core-js
的 microtask
模块- 优先使用
queueMicrotask
方法
- 浏览器可以使用
MutationObserver
, 除了 iOS 环境
- 有些环境实现了非完全正确的
Promise
(例如WebKit ~ iOS Safari 10.1
), 可以使用Promise.then
实现
NodeJS
使用process.nextTick
- 其他环境使用
macrotask
实现
宏任务基本原理
核心微任务代码实现见
core-js
的 task
模块- 优先使用
setImmediate
和clearImmediate
- 早期 NodeJS 0.8 - 没有
immediate
, 使用process.nextTick
- Sphere (JS game engine) 使用
Dispatch.now
- 浏览器使用
MessageChannel
除了 iOS 环境
- 其他浏览器(除了 IE8 -), 使用
global.postMessage
和global.addEventListener('message', ...)
- 其他浏览器使用
html.appendChild(createElement('script'))['onreadystatechange']
- 其余的
setTimeout
兜底
特殊示例
关于 setTimeout/setInterval 0 延迟
The
HTML5 standard
says: “after five nested timers, the interval is forced to be at least 4 milliseconds.”结果说明:
- 在浏览器中如果 setTimeout 被嵌套了 5 次, 之后的回调间隔至少为 4ms
- 在 NodeJS 中执行没有这个限制
原因分析:
- 如果没有延迟有可能会导致 UI 锁死, CPU 过载
- 操作系统的 clock 本身也不是很准确
相关代码:
setTimeout 示例代码
let start = Date.now();
let times = [];
setTimeout(function run() {
times.push(Date.now() - start);
// remember delay from the previous call
if (start + 100 < Date.now()) {
console.log(times);
} else {
// show the delays after 100ms
setTimeout(run);
}
});
// an example of the output:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100ÏÏ
setInterval 示例代码
let start = Date.now();
let times = [];
const timmerId = setInterval(function run() {
times.push(Date.now() - start);
// remember delay from the previous call
if (start + 100 < Date.now()) {
clearInterval(timmerId);
console.log(times);
}
});
关于 process.nextTick 和 Promise.then
process.nextTick 比 Promise.then 先执行
process.nextTick(() => {
console.log(1);
process.nextTick(() => {
console.log(2);
});
});
new Promise((resolve) => {
console.log(3);
resolve();
}).then(() => {
console.log(4);
});
process.nextTick(() => {
console.log(5);
});
// 输出 3,1,5,2,4
process.nextTick 注册的回调会在事件循环的当前阶段结束前执行
/**
* 执行栈中注册 setTimeout 计时器
*/
setTimeout(function () {
// 4. timers 阶段。timer watcher 遍历计时器 Map 的 key,
// 如果有 key <= timeout,执行该 key 对应的 value(计时器任务);
// 否则等到 poll 阶段再检查一次
console.log("setTimeout");
setTimeout(function () {
// 11. 注册 setTimeout 计时器。UV_RUN_ONCE 模式下,
// 会在循环结束之前再执行时间下限到达的计时器任务,取决于进程性能
// 1 <= TIMEOUT_MAX <= 2 ^ 31 - 1
console.log("setTimeout in setTimeout");
}, 0);
setImmediate(function () {
// 9. 注册 setImmediate 计时器。在当前循环的 check 阶段执行。
// (注:这是新的 ImmediateList,当前循环内有 3 个 ImmediateList 了)
console.log("setImmediate in setTimeout");
});
process.nextTick(function () {
// 6. 为 nextTickQueue 添加任务,timers 阶段结束前唤醒 idle watcher
// idle watcher 检查 nextTickQueue,执行任务
console.log("nextTick in setTimeout");
});
}, 0);
/**
* 执行栈中注册 setImmediate 计时器
*/
setImmediate(function () {
// 7. poll 阶段没有可执行任务,阶段结束前唤醒 idle watcher,idle watcher 继续睡;
// 接着唤醒 check watcher,检测到 ImmediateList 不为空,进入 check 阶段。
// check watcher 执行第一个任务
console.log("setImmediate");
setTimeout(function () {
// 13. 注册 setTimeout 计时器
// 由于机器性能,在循环结束前才执行
console.log("setTimeout in setImmediate");
}, 0);
setImmediate(function () {
// 12. 为当前 ImmediateList 添加任务
// 由于机器性能优越,前面 nextTickQueue 为空了,直接进入 check 阶段
console.log("setImmediate in setImmediate");
});
process.nextTick(function () {
// 10. 为 nextTickQueue 添加任务,当所有 ImmediateList 的队首任务都执行完毕时,
// 唤醒 idle watcher,检查 nextTickQueue,执行队列任务
console.log("nextTick in setImmediate");
});
});
/**
* 执行栈中为 nextTickQueue 添加任务
*/
process.nextTick(function () {
// 2. 执行栈为空,进入事件循环准备阶段,唤醒 prepare watcher,
// 检查 nextTickQueue,执行队列中的任务
console.log("nextTick");
setTimeout(function () {
// 5. 注册计时器任务,timers 阶段到达时间下限则执行该任务,
// 否则等到 poll 阶段
console.log("setTimeout in nextTick");
}, 0);
setImmediate(function () {
// 8. 注册 setImmediate 计时器,在当前循环的 check 阶段执行。
// (注:这是新的 ImmediateList,当前循环内有 2 个 ImmediateList 了)
console.log("setImmediate in nextTick");
});
process.nextTick(function () {
// 3. prepare watcher 处于活跃状态,检测 nextTickQueue 的新任务,
// 执行完所有任务后沉睡
console.log("nextTick in nextTick");
});
});
console.log("main thread"); // 1. 执行栈的任务
关于 Promise.then 和 Promise.resolve 执行顺序
示例1
Promise.resolve()
.then(() => {
// mt1
console.log(0);
// 以下语法同 Promise.resolve()
return new Promise(resolve => {
// 内部为 mt8
resolve();
});
})
.then(() => {
// mt2
console.log(4);
});
Promise.resolve()
.then(() => {
// mt3
console.log(1);
})
.then(() => {
// mt4
console.log(2);
})
.then(() => {
// mt5
console.log(3);
})
.then(() => {
// mt6
console.log(5);
});
/**
* 1. Stack: null; nextTick: mt1, mt3 => none
* 2. Stack: mt1, mt3; nextTick: null => none
* 3. Stack: mt3; nextTick: mt8 => 0
* 4. Stack: null; nextTick: nextTick(mt8), mt4 => 1
* 5. Stack: mt4; nextTick: mt8 => none
* 6. Stack: null; nextTick: mt8, mt5 => 2
* 7. Stack: mt8, mt5; nextTick: null => none
* 8. Stack: mt5; nextTick: mt2 => none
* 9. Stack: null; nextTick: mt2, mt6 => 3
* 10. Stack: mt2, mt6; nextTick: null => none
* 11. Stack: mt6; nextTick: null => 4
* 12. Stack: null; nextTick: mt7 => 5
* 13. Stack: mt7; nextTick: null => none
* 14. Stack: null; nextTick: null => 6
*/