# 防抖和节流

# 前言

示例:codepen (opens new window)

# 一、概念如何理解?

# 防抖

概念:触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。 可以理解为游戏中法师的施法前摇,1s的前摇如果没有被打断,则施法成功;如果1s内被打断,则重新开始计时。

# 节流

概念:高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。 可以理解为FPS游戏的射速,就算一直按着鼠标射击,也只会在规定射速内射出子弹。

# 相同点

  • 目的都是,降低回调执行频率。节省计算资源。

# 不同点

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout 和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
  • 函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次。

# 函数防抖的应用场景

连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

# 函数节流的应用场景

间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 谷歌搜索框,搜索联想功能
  • 高频点击提交,表单重复提交

# 二、如何实现?

# 1、基础版

快速请求,连续发送了多个相同的api,这样容易对服务器造成较大压力

// 模拟api请求
function ajax(api) {
    console.log('request: ', api)
}
// 模拟快速提交
for (let i = 1; i <= 10; i++) {
    ajax('/user');
}
1
2
3
4
5
6
7
8

使用节流解决:

// 节流
// 效果:一定时间间隔内,只调用一次fn
function throttle(fn, wait) {
  let lastTime = null
    return (args) => {
        if (!lastTime || Date.now() - lastTime >= wait) {
            fn(args)
            lastTime = Date.now()
        }
    }
}

let throttleAjax = throttle(ajax, 1000);

// 测试
for (let i = 1; i <= 10; i++) {
    setTimeout(() => {
        console.log(i)
        throttleAjax('/user')
    }, 500 * i)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

使用防抖解决:


// 防抖
function debounce(fn, wait) {
    let timer = null
    return (args) => {
        clearTimeout(timer)
        timer = setTimeout(() => {
            fn(args)
        }, wait)
    }
}
let debounceAjax = debounce(ajax,1000)

// 测试
for (let i = 1; i <= 10; i++) {
    setTimeout(() => {
        console.log(i)
        debounceAjax('/user')
    }, 500 * i)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 2、升级版

支持 leading 和 trailing 配置

  • leading:第一次触发时执行
  • trailing:触发完毕后执行一次
function ajax(api) {
  console.log('request: ', api);
}

function debounce(fn, delay, options = {}) {
  let leading = options.leading !== undefined ? !!options.leading : false; // 首次触发后立即执行,默认为false
  let timer = null;
  let firstInvoked = true;
  return function() {
    let context = this;
    let args = arguments;

    if (firstInvoked) {
      firstInvoked = false;
      if (leading) {
        fn.apply(context, args);
        return;
      }
    }

    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, delay);
  }
}


function throttle(fn, delay, options = {}) {
  let leading = options.leading !== undefined ? !!options.leading : true; // 首次触发后立即执行,默认为true
  let trailing = options.trailing !== undefined ? !!options.trailing : true; // 触发结束后是否执行一次,默认为true
  let lastTime = null;
  let timer = null;
  let firstInvoked = true;
  return function() {
    let context = this;
    let args = arguments;
    let remaining = delay - (Date.now() - lastTime);

    function invokeFn(isTrailing) {
      fn.apply(context, args);

      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastTime = Date.now();
    }

    if (firstInvoked) {
      leading && invokeFn();
      firstInvoked = false;
      return;
    }
    
    if (!lastTime || remaining <= 0) {
      invokeFn();
    } else if (trailing && !timer) {
      timer = setTimeout(() => {
        invokeFn()
      }, delay);
    }
  };
}

let throttleAjax = throttle(ajax, 3000, {leading: true, trailing: true });
let debounceAjax = debounce(ajax, 3000, {leading: false});

// 测试
for (let i = 1; i <= 10; i++) {
  setTimeout(() => {
    console.log(i);
    debounceAjax("/user");
    // throttleAjax("/user");
  }, 1000 * i);
}
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

# 参考资料

上次更新: 1/13/2022, 8:26:50 AM