js 基础 - 事件循环 & 垃圾回收机制
2023-10-24 04:53:11 # fontend

1. 什么是事件循环(EventLoop)

为了解决单个任务执行时间过长和处理高优先级的任务,所以需要将任务划分, 引入同步任务和异步任务解决执行时间过长的问题,同步任务在执行栈中直接执行,异步任务放入任务队列等待执行;为了解决异步队列中等待任务的执行优先级问题,将异步任务划分为宏任务和微任务,同步任务执行完后,先执行微任务,然后执行宏任务,以此循环,形成事件循环。

2. 宏任务(macrotask) & 微任务(microtask)

  • 微任务: 微任务仅来自于我们的代码,通常由 promise 创建,优先级比宏任务高,属于异步任务。
  • 宏任务: 一般的异步任务。

可以创建微任务的:

  1. promise
  2. async await
  3. queueMicrotask

可以创建宏任务的:

  1. setTimeout / setInterval
  2. script 标签脚本
  3. 事件回调函数: DOM Events, I/O, requestAnimationFrame 等

3. 异步任务相关的面试题

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// 1 考察 promise 和 async
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

// 打印结果
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout


// 2 考察 setTimeout 与 promise
const tasks = []
const output = (i) => new Promise((reslove) => {
setTimeout(() => {
console.log(i);
reslove();
}, 1000 * i)
});

for(var i = 0; i<5; i++) {
tasks.push(output(i))
}

Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i)
}, 1000)
});

// 打印结果
0 1 2 3 4 5

// 3 setTimout 和 sleep
setTimeout(() => { console.log(1) }, 1000)

setTimeout(() => { console.log(2) }, 0)

setTimeout(() => {
console.log(3)
setTimeout(() => { console.log(4) }, 0)
}, 800)
function sleep(delay) {
var start = (new Date()).getTime();
while((new Date()).getTime() - start < delay) {
continue;
}
}

sleep(1200)
console.log(5)
for(var i=10;i<=15;i++) {
setTimeout(() => {console.log(i)}, 0)
}

// 打印结果
5 2 16*6 3 1 4

// 4
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3'); // 比 children5 先执行
})
}, 0);

new Promise(function(resolve, reject) {
console.log('children4');
setTimeout(function() {
console.log('children5');
resolve('children6')
}, 0)
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0)
})

// 说明:在没有 reslove 的时候,不会将 then 创建为微任务
// 执行结果
start
children4
children2
children3
children5
children7
children6

// 5
const p = function() {
return new Promise((resolve, reject) => {
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
resolve(2)
})
p1.then((res) => {
console.log(res);
})
console.log(3);
resolve(4);
})
}


p().then((res) => {
console.log(res);
})
console.log('end');

// 打印结果
3 ‘end’ 2 4

3. 垃圾回收机制(garbage collection)

为什么需要垃圾回收:
因为 js 运行期间会生成变量,函数这些占用内存空间的存储,如果这些内存不及时释放,则会造成内存过大,js 运行过载的情况。所以不再使用的变量活着函数需要被及时回收,以免造成内存泄漏。

回收机制原理

  1. 标记清理:被执行时标记存在上下文,执行完毕清除标记,被回收
    1. 优点: 实现比较简单
    2. 缺点: 清除之后,剩余对象的内存位置不变,会导致空闲内存空间不是连续的,会牵扯出内存分配效率慢等问题
  2. 引用计数:追踪值被引用的次数,当引用次数为 0 的时候则被回收,闭包的原理就是因为函数中引用的变量计数没有归 0 所以不会被回收
    1. 优点: 可以立即回收,标记清理需要定期清理,另外标记需要遍历,引用则不需要
    2. 缺点: 引用计数需要一个计数器,会占用很多内存;无法解决循环引用无法回收的问题

v8 针对 gc 做出的优化

  1. 分代式回收
    1. 针对变量或对象存活的时间长短,大小,生成时间的新旧来区分为新生代和老生代,在存储上也分别存在不同的空间,新生代分配的内存空间 1-8M 左右,对应的 gc 算法是 scavenge 效率高,老生代占用的内存偏大,对应采用的是 mark-compact-sweep,效率低些
    2. scavenge 算法: 其具体实现主要采用了 Cheney 算法,将新生代的堆内存空间分为 处于使用中的 from 空间和处于闲置的 to 空间,当分配对象时,会先放到from 空间中。垃圾回收时遍历 from 空间,将还存活的对象复制到 to 空间,然后 from 和 to 空间角色互换。当一个对象经过多次复制依然存活的时候,就会被晋升到老生代内存空间中。
    3. mark- sweep & mark-compact 算法:
      1. mark- sweep: 标记清除,定期清除不在存活的对象
      2. mark-compact: 标记整理,就是在清除之后,如果发现老生代分配空间不足时,对新生代晋升过来的对象分配时采用 mark-compact 对内存空间重新分配
  2. 并行回收: 因为 js 是单线程运行的,gc 运行时也是会阻塞 js 执行的,为了加快 gc 回收,引入了并行回收,开启多个辅助线程,协同完成 gc 回收工作
  3. 并发回收:采用并行回收还是会多少存在 js 脚本阻塞的问题,为了从根本上解决这个问题,采用了并发回收机制,gc 回收完全在辅助线程上进行,不占用主线程,丝毫不会导致阻塞 js 脚本。但是要实现并发很难,主线程在执行 js 的时候,堆中的对象引用关系随时在变化,所以辅助线程的标记也会改变,所以需要额外实现一些读写锁的机制来控制。