# 防抖节流

# 防抖

简单的防抖实现

// func是用户需要传入的方法
// wait是防抖间隔
const debounce = (func, wait = 50) => {
  let timer = 0
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
1
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()
    }
  }
}
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

# 节流

防抖是多次执行变成最后一次执行,节流则是多次执行变成每个一段时间执行

// 获取当前时间戳
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;
  }
}
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

# 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
};
1
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 运行,你可以点击 该博客 详细阅读。

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象像一端移动,直到所有对象都移动完成然后清理掉不需要的内存