# 节流 防抖

# 背景

  • 频繁触发事件 回调频繁被执行 影响前端性能
    例如 resize scroll keypress 等
  • 优化高频率执行代码的一种手段

# 经典比喻

  • 电梯的运行策略,定义超时事件为15s
    节流:电梯第一个人进来后,15秒后准时运送一次
    防抖:电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送

# 防抖

  • 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次, 如果设定的时间到来之前,又一次触发了事件,就重新开始延时

# 不同版本

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Debounce</title>
</head>
<body>
  <div id="el"></div>
  <button onclick="cancel()">取消</button>
</body>
<script>
  const el = document.getElementById('el')
  // el.onmousemove = fn1
  // el.onmousemove = debounce2(fn2, 1000)
  // el.onmousemove = debounce3(fn3, 1000)
  // el.onmousemove = debounce4(fn4, 1000)
  // 立即执行
  // el.onmousemove = debounce5(fn5, 1000, true)
  // 取消
  const debounce = debounce6(fn5, 3000, true)
  el.onmousemove = debounce

  const cancel = () => {
    debounce.cancel()
  }

  let count = 1

  // 不使用 防抖
  function fn1(e) {
    console.log(this); // this 指向 <div id="el"></div>
    console.log(e); // Event
    el.innerHTML = count++
  }

  // 第一版防抖
  // this 指向 window
  // 事件参数为 undefined
  function debounce2(fn, delay) {
    let timer = null
    return function() {
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn()
      }, delay);
    }
  }

  function fn2(e) {
    console.log(this); // this 指向 window
    console.log(e); // undefined
    el.innerHTML = count++
  }

  // 第二版
  // 改变 this 指向 
  // 回调函数传参
  function debounce3(fn, delay) {
    let timer = null
    return function() {
      let self = this
      let args = arguments
      clearTimeout(timer)
      timer = setTimeout(() => {
        fn.apply(self, args)
        // or
        // fn.apply(this, arguments)
      }, delay);
    }
  }

  function fn3(e) {
    console.log(this); // this 指向 window
    console.log(e); // undefined
    el.innerHTML = count++
  }

  // 第三版 增加返回值
  function debounce4(fn, delay) {
    let timer = null
    let result
    return function() {
      let self = this
      let args = arguments
      clearTimeout(timer)
      timer = setTimeout(() => {
        result = fn.apply(self, args)
      }, delay);

      return result
    }
  }

  function fn4() {
    el.innerHTML = count++

    return 'hello'
  }

  // 立即执行 
  function debounce5(fn, delay, immediate = false) {
    let timer = null

    return function() {
      let self = this
      let args = arguments

      if (timer) clearTimeout(timer)
      if (immediate) {
        let isApply = !timer
        timer = setTimeout(() => {
          timer = null
        }, delay);
        if (isApply) fn.apply(self, args)
      } else {
        timer = setTimeout(() => {
          fn.apply(self, args)
        }, delay);
      }
    }
  }

  function fn5() {
    el.innerHTML = count++
  }

  // 取消
  function debounce6(fn, delay, immediate) {
    let timer = null

    const debounced = function() {
      let self = this
      let args = arguments

      if (timer) clearTimeout(timer)
      if (immediate) {
        let isApply = !timer
        timer = setTimeout(() => {
          timer = null
        }, delay);
        if (isApply) fn.apply(self, args)
      } else {
        timer = setTimeout(() => {
          fn.apply(self, args)
        }, delay);
      }
    }

    debounced.cancel = function() {
      // clearTimeout(timer)清除了timer指向的定时器;timer=null,是修改timer的指向
      clearTimeout(timer)
      timer = null
    }

    return debounced
  }

</script>
</html>

<style>
  div {
    width: 200px;
    height: 200px;
    border: 2px solid;
  }
</style>
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
  • 针对滚动事件
// 滚动事件
// 不使用防抖
window.addEventListener('scroll', handle);
// 使用 防抖
window.addEventListener('scroll', debounce(handle, 1000));
1
2
3
4
5
  1. 不使用 防抖
function debounce(fn, wait) {
  var timeout = null;
  return function() {
    if (timeout !== null) clearTimeout(timeout);
    timeout = setTimeout(fn, wait);
  }
}

// 处理函数
function handle(e) {
  console.log(this); // 
  console.log(e); 
  console.log(Math.random()); 
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function debounce(fn, wait) {
  var timeout = null;
  return function() {
    if (timeout !== null) clearTimeout(timeout);
    timeout = setTimeout(fn, wait);
  }
}
// 处理函数
function handle() {
  console.log(Math.random()); 
}

// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));

// debounce 返回一个 fn 功能函数 
// 然后在 click 事件中执行 handleClick()
const handleClick = debounce(handle, 1000)

<button onclick="handleClick()">debounce</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 节流

  • 当持续触发事件时,保证一定时间段内只调用一次事件处理函数
// 时间戳写法 事件会立即执行
var throttle = function(func, delay) {
  var prev = Date.now();
  return function() {
    var context = this;
    var args = arguments;
    var now = Date.now();
    if (now - prev >= delay) {
      func.apply(context, args);
      prev = Date.now();
    }
  }
}

function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));

// 定时器写法 delay后执行一次
var throttle = function(func, delay) {
  var timer = null;
  return function() {
    var context = this;
    var args = arguments;
    if (!timer) {
        timer = setTimeout(function() {
          func.apply(context, args);
          timer = null;
        }, delay);
    }
  }
}

function handle() {
  console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
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
  • 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,
    而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,
    我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。
    这样的场景,就适合用节流技术来实现。

# 相同点

  • 都可以使用 setTimeout 实现

  • 目的相同,节省计算资源,降低执行频率

# 不同点

  • 以delay为1000ms为例,防抖,在1000ms内触发多次,但只执行最后一次;节流,每个1000ms执行一次

# 应用场景

  • 防抖,搜索框输入,窗口resize

  • 节流,滚动加载,加载更多或滚动到底部