# 防抖节流
# 防抖
简单的防抖实现
// func是用户需要传入的方法
// wait是防抖间隔
const debounce = (func, wait = 50) => {
let timer = 0
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
2
3
4
5
6
7
8
9
10
11
这个防抖有个缺陷,防抖只能在最后调用,没有立即调用的选项
- 例如百度搜索,总是输完最后一个字才查询,那就是和延迟查询
- 而像点赞之类的,总是立即调用,并且下一次调用必须和上一次间隔大于wait才会触发
带有立即执行选项的防抖函数
// 防抖函数,当函数连续调用时,空闲时间必须大于wait才执行
function debounce(func, wait = 50, immediate = true) {
let timer, context, args
const later = () => setTimeout(() =>{
timer = null
// 延迟执行,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if(!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
return function(...params) {
if(!timer) {
timer = later()
if(immediate) {
func.apply(this, params)
}else{
context = this
args = params
}
} else {
// 如果不是延迟执行的,重新设置timer时间到了会把timer置空,这样就能立即执行了
clearTimeout(timer)
timer = later()
}
}
}
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
# 节流
防抖是多次执行变成最后一次执行,节流则是多次执行变成每个一段时间执行
// 获取当前时间戳
function now() {
return +new Date()
}
function throttle(func, wait, options) {
let context, args, result
let timeout = null
var previous = 0
// 如果 options 没传则设为空对象
if (!options) options = {};
var later = function() {
// 如果设置了 leading,就将 previous 设为 0
// 用于下面函数的第一个 if 判断
previous = options.leading === false ? 0 : now();
timeout = null
result = func.apply(context, args)
if(!timeout) context = args = null
}
return function() {
var now = now()
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
// 如果存在定时器就清理掉否则会调用二次回调
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判断是否设置了定时器和 trailing
// 没有的话就开启一个定时器
// 并且不能不能同时设置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
}
}
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
# V8下的垃圾回收机制
V8实现了准确式GC,GC算法采用了分代式垃圾回收机制。因此V8将内存(堆)分成了新生代和老生代两部分
# 新生代算法
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
# 老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
在讲算法前,先来说下什么情况下对象会出现在老生代空间中:
- 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
老生代中的空间很复杂,有如下几个空间
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不变的对象空间
NEW_SPACE, // 新生代用于 GC 复制算法的空间
OLD_SPACE, // 老生代常驻对象空间
CODE_SPACE, // 老生代代码对象空间
MAP_SPACE, // 老生代 map 对象
LO_SPACE, // 老生代大空间对象
NEW_LO_SPACE, // 新生代大空间对象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在老生代中,以下情况会先启动标记清除算法:
- 某一个空间没有分块的时候
- 空间中被对象超过一定限制
- 空间不能保证新生代中的对象移动到老生代中
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行,你可以点击 该博客 详细阅读。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存