js - 防抖

ru shui 2021-06-20 Javascript
  • Javascript
  • Throttle
About 4 min

# 引例

我们现在需要实现这样的功能:每隔 n 秒触发一个事件。这个需求与防抖类似,最大的区别是 n 秒后还可以继续执行该事件的回调函数。

# 基本概念

节流:持续触发事件,则每隔一段时间才会执行该事件。

# 实现

节流的只要实现有两种方式:

  • 时间戳
  • 定时器

下面我们将一一道来。

# version1: 时间戳

function throttle(fn: Function, wait = 1000): Function {
  let previous = 0
  return function (...args: any[]) {
    let now = +new Date()
    let result: any

    if (now - previous >= wait) {
      result = fn.apply(this, args)
      previous = now
    }
    return result
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

采用时间戳的方式特别简单,只需要判读当前时间减去之前的时间是否超过要求,是的话就执行函数即可。需要注意的点就是,执行完函数后,记得将previous = now

# version2: 定时器

function throttle(fn: Function, wait = 3000): Function {
  let timeout: number = null
  let result: any
  return function (...args: any[]) {
    if (timeout === null) {
      // result = fn.apply(this, args)
      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
    return result
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

使用定时器与使用时间戳的区别在于,定时器可以让我们延长执行回调函数。

既然两种实现方式各有特色,那么我们能否将两种实现方式组合起来,实现特殊需求?

# version3: 时间戳 + 定时器

# 1. 触发后立刻执行,停止后再次执行

function throttle(fn: Function, wait = 1000): Function {
  //* in order to execute callback the first time
  let previous = 0
  let timeout: number | null = null
  const clear = () => {
    clearTimeout(timeout)
    timeout = null
  }

  return function (...args: any[]) {
    let result: any
    let now = +new Date()
    // debugger

    // the rest time to execute
    //! to avoid the time of system has been modify
    // if time has been forward, it will be very dangerous
    // if time has been backward, it will be waiting for more time to execute.
    let remaining = wait - (now - previous)

    if (remaining <= 0 || remaining > wait) {
      if (timeout !== null) clear()
      previous = now
      result = fn.apply(this, args)
    } else if (timeout === null) {
      timeout = setTimeout(() => {
        fn.apply(this, args)
        previous = +new Date()
        timeout = null
      }, 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

这里就是将上面两个版本进行重叠,然后处理连接逻辑即可。 先看最初的实现:

function throttle(fn: Function, wait = 1000): Function {
  let previous = 0
  let timeout: number | null = null

  const clear = () => {
    clearTimeout(timeout)
    timeout = null
  }
  return function (...args: any[]) {
    let result: any
    let now = +new Date()

    let remaining = wait - (now - previous)
    if (remaining <= 0 || remaining > wait) {
      previous = now
      result = fn.apply(this, args)
    }

    if (timeout === null) {
      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
    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

我们只是将超时的计算换成remaining = wait - (now - previous),这样做的好处在于可以放置用户修改时间,其次就是在等下的逻辑连接处,可以正确计算剩余时间。

关键点在于如何处理连接。 事实上,从上面的两个版本我们可以知道,只要其中一个版本就可以实现每隔 n 秒执行一次回调函数。所以我们可以采用一个 if-else if 来确保当前只有一个版本在执行。 也就是其中的:

if (remaining <= 0 || remaining > wait) {
} else if (timeout === null) {
}
1
2
3

其次,我们还需要处理一下中间的逻辑:

  1. 当执行时间戳版本时,确保定时器版本不会执行。
  2. 当执行定时器版本时,确保时间戳版本不会执行。

转换成如下代码:

if (remaining <= 0 || remaining > wait) {
  if (timeout !== null) clear() // ensure timer version will not execute.
} else if (timeout === null) {
  previous = +new Date() // ensure timestamp version will not execute.
}
1
2
3
4
5

# version4: leading && trailing




















 












 











function throttle(
  fn: Function,
  wait = 1000,
  { leading = true, trailing = false }: LeadingAndTrailing = {
    leading: true,
    trailing: false
  }
): Function {
  //* in order to execute callback the first time
  let previous = 0
  let timeout: number | null = null
  const clear = () => {
    clearTimeout(timeout)
    timeout = null
  }

  return function (...args: any[]) {
    let result: any
    let now = +new Date()
    previous = leading ? previous : now
    // debugger

    // the rest time to execute
    //! to avoid the time of system has been modify
    // if time has been forward, it will be very dangerous
    // if time has been backward, it will be waiting for more time to execute.
    let remaining = wait - (now - previous)

    if (remaining <= 0 || remaining > wait) {
      if (timeout !== null) clear()
      previous = now
      result = fn.apply(this, args)
    } else if (timeout === null && trailing) {
      timeout = setTimeout(() => {
        fn.apply(this, args)
        previous = +new Date()
        timeout = null
      }, 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
43

我们值需修改其中两行代码就可以实现该功能: previous = leading ? previous : now,如果我们需要触发事件时就执行回调函数,这个时候previous = previous也就是跟我们之前实现一样,当leading = false时,我们调整一下时间,使得remaining落在在不执行区间里面,所以不会立刻执行回调函数。

同样的道理,我们只需要在离开时执行回调函数处,加以判断即可,也就是else if (timeout === null && trailing)

# version4: leading && trailing

# version6: 取消

throttledFn.cancel = () => {
  clear()
  previous = 0
}
1
2
3
4

我们只需要将返回的函数作为匿名函数,同时,添加上上面的取消函数即可。

# 应用场景

和防抖一样,节流也是应用在高频触发的函数上。节流常用的高频函数有输入框的输入等。 以输入框的输入为例,当用户输入时,我们会根据用户的输入返回特定的提示,这里需要用到就是节流而非防抖。

# reference

Last update: June 20, 2021 08:56
Contributors: Laishuxin