-.- -.-
首页
  • 分类
  • 标签
  • 归档
  • Lodash 源码分析 (opens new window)
Mozilla (opens new window)
GitHub (opens new window)

江月何年初照人

首页
  • 分类
  • 标签
  • 归档
  • Lodash 源码分析 (opens new window)
Mozilla (opens new window)
GitHub (opens new window)
  • 算法

  • JS相关内容

    • 从深拷贝看JS中的循环引用
    • 0.1+0.2 为什么不等于 0.3
    • JS中的位运算
    • 稀疏数组与稠密数组
    • this到底指向谁
    • JS中的比较规范
    • 连续赋值
    • 跨域及解决方案
    • 节流和防抖
      • 为什么需要节流和防抖
      • 什么是节流和防抖,它的作用是什么
      • underscore.js
        • throttle
        • debounce
      • lodash
        • 首先,lodash 中 debounce 的流程是怎样的
        • 接着根据流程图,看看怎么实现这么一个函数
    • 从输入 URL 到页面展示,这中间发生了什么?
  • 基础

  • js
  • JS相关内容
江月何年初照人
2021-03-25

节流和防抖

# 节流和防抖

# 为什么需要节流和防抖

在日常我们写代码的过程中,会出现很多的高频事件,比如浏览器页面的滚动事件 onscroll,浏览器窗口的缩放事件 resize,这些事件在我们触发时,会执行回调函数很多次,有可能 1s执行了好几十次,但是对于功能需求而言,我们可能只需要最后一次的执行结果,或者希望它的执行有一个间隔时间,比如 1s 执行一次,那么这个时候就需要用到 节流和防抖 来对回调函数的执行加一个限制

# 什么是节流和防抖,它的作用是什么

节流(throttle)

节流就是保证一段时间内,核心代码只会执行一次。

简单的节流函数

function throttle (func, wait) {
  let last = 0
  return function () {
    const now = + new Date
    if (now - last > wait) {
      func.apply(this, arguments)
      last = now
    }
  }
}
1
2
3
4
5
6
7
8
9
10

防抖(debounce)

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

简单的防抖函数

function debounce(func, wait) {
  let timer
  return function () {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9

简单的 节流和防抖 就这样就可以实现了,接下来我们来看看比较出名的两个库,看看他们对于 节流和防抖 是怎么处理的

# underscore.js

# throttle

首先在定义函数时,和简易版的比起来,多了 options 配置,可以做一些额外的操作,比如 trailing 表示是否在在延时结束后调用 func

function throttle(func, wait, options) {}
1

定义了返回 函数 throttled 以及使用到的变量,实现简单的节流

function throttle(func, wait, options) {
  let args, context, last = 0
  const throttled = () => {
    args = arguments
    context = this
    const now = +new Date
    const remaning = wait - (now - last)
    if (remaning <= 0 || remaning > wait) { // 第一次会触发 或者 wait 时间到了会触发
      func.apply(context, args)
      last = now
    }
  }
  return throttled
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

这个时候 options 还是没有用到, 接下来要处理 options 的内容

function throttle(func, wait, options = {}) {
  // trailing 最后一次默认是触发的
  let args, context, last = 0, timer // 添加一个定时器 用来计算最后一次
  const later = () => { // 最后一次执行的方法
    timer = null
    func.apply(context, args)
    last = +new Date
  }
  const throttled = () => {
    args = arguments
    context = this
    const now = +new Date
    const remaning = wait - (now - last)
    if (remaning <= 0 || remaning > wait) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      func.apply(context, args)
      last = now
      return
    } else if (!timer && options.trailing !== false) {
      timer = setTimeout(later, remaning) // 定时器为最后剩余的时间,也就是 wait 减去 已经度过的时间
    }
  }
  return throttled
}
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

处理 leading , 判断一开始是否执行,完善一下逻辑就实现了一个节流函数

function throttle(func, wait, options = {}) {
  // trailing 最后一次默认是触发的
  let args, context, last = 0, timer
  const later = () => {
    last = options.leading === false ? 0 : +new Date
    func.apply(context, args)
    args = context = null
  }
  const throttled = () => {
    args = arguments
    context = this
    const now = +new Date
    if (!last && options.leading === false) last = now // 如果 leading 为 false , 那么此时将 last 置为 now ,那么第一次就不会触发 事件执行
    const remaning = wait - (now - last)
    if (remaning <= 0) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      func.apply(context, args)
      last = now
    } else if (!timer && options.trailing !== false) { // last 置为 now 就会走到这里
      timer = setTimeout(later, remaning)
    }
  }
  return throttled
}
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

# debounce

underscore 在简单版本的基础上,新增加了一些 options 配置,核心逻辑就是这些东西,在源码中还添加了 cancel 等方法,这里就不赘述了,实现核心逻辑就可以了

function debounce(func, wait, immediate) {
  // immediate 第一次是否触发事件
  let timer
  return function () {
    clearTimeout(timer)
    if (immediate && !timer) func.apply(this, arguments)
    timer = setTimeout(() => {
      func.apply(this, arguments)
      timer = null
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# lodash

lodash 对于 节流和防抖的处理是放在同一个函数中的,也就是 debounce,throttle 是引用的 debounce 实现的,之前在分析 lodash 源码时,对于 debounce 的源码做了一个分析 (opens new window) ,那么接下来我们来看看怎么一步一步实现

# 首先,lodash 中 debounce 的流程是怎样的

# 接着根据流程图,看看怎么实现这么一个函数

首先创建一个 简单的 防抖函数

function debounce(func, wait) {
  let timer
  return function () {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9

对于当前函数,并没有做参数合法化的校验,我们添加这一段逻辑,对于 func 我们希望它是一个函数,如果不是则抛出类型错误,对于 wait 希望它是一个数字,使用 一元正号进行转换,有可能为 NaN , 如果为 NaN 则取 0

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  
  wait = +wait || 0
  
  let timer
  return function () {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

对于 防抖和节流,我们都希望它第一次和最后一次默认都触发,那么可以定义两个标识

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  
  wait = +wait || 0
  
  let timer
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发
  
  function debounced () {
    clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, wait)
  }
  
  return debounced
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

现在我们使用的是 setTimeout 来实现防抖,现代浏览器有一个 requestAnimationFrame 的 api ,会在浏览器重绘之前调用,性能比 setTimeout 更好

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let timer
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发
  
  function debounced () {
    if (useRAF) {
      window.cancelAnimationFrame(timer)
    } else {
      clearTimeout(timer)
    }

    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      timer = window.requestAnimationFrame(() => {
        func.apply(this, arguments)
      })
    } else {
      timerId = setTimeout(() => {
        func.apply(this, arguments)
      }, wait)
    }
  }
  
  return debounced
}
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

可以看到 此时 debounced 的处理逻辑 就已经有点复杂了,中间的 if 判断 比较多,那么可以考虑抽离成两个单独的函数来处理 定时器的开启 (startTimer) 和 清除(clearTimer)

提取方法后,针对 this 和 arguments 就需要缓存下来

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let timer
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发
  
  let lastThis; // 返回函数的 this
  let lastArgs; // 返回函数的参数
  
  // 开启定时器
  function startTimer (func, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(() => {
        func.apply(lastThis, lastArgs)
      })
    }
    return setTimeout(() => {
      func.apply(lastThis, lastArgs)
    }, wait)
  }
  
  // 清除定时器
  function clearTimer (timerId) {
    if (useRAF) {
      return window.cancelAnimationFrame(timerId)
    }
    clearTimeout(timerId)
  }
  
  function debounced (...args) {
    lastThis = this
    lastArgs = args
    clearTimer(timer)
    timer = startTimer(func, wait)
  }
  
  return debounced
}
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

现在我们可以发现,在 setTimeout 和 requestAnimationFrame 中,对于执行函数这里的逻辑是一致的,可以考虑提成一个单独的方法来处理(invokeFunc)

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发

  let timer,
      lastThis, // 返回函数的 this
      lastArgs, // 返回函数的参数
      result; // 最后的返回结果
  
  // 执行函数
  function invokeFunc () {
    let args = lastArgs
    let thisArg = lastThis
    
    result = func.apply(thisArg, args)
    lastArgs = lastThis = undefined
    return result
  }
  
  // 开启定时器
  function startTimer (pendingFunc, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  
  // 清除定时器
  function clearTimer (timerId) {
    if (useRAF) {
      return window.cancelAnimationFrame(timerId)
    }
    clearTimeout(timerId)
  }
  
  function debounced (...args) {
    lastThis = this
    lastArgs = args
    clearTimer(timer)
    timer = startTimer(invokeFunc, wait)
  }
  
  return debounced
}
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

现在我们要来处理,来判断,debounced 的方法 是否需要执行,包含第一次进来的事件触发也在这个判断里

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发

  let timer,
      lastThis, // 返回函数的 this
      lastArgs, // 返回函数的参数
      result, // 最后的返回结果
      lastCallTime; // 最后调用的时间
  
  // 执行函数
  function invokeFunc () {
    let args = lastArgs
    let thisArg = lastThis
    
    result = func.apply(thisArg, args)
    lastArgs = lastThis = undefined
    return result
  }
  
  function shouldInvoke () { // 返回 布尔值
    // 第一次
    return lastCallTime === undefined
  }
  
  // 是否第一次执行
  function leadingEdge () {
    if(leading) invokeFunc() // 如果需要则执行函数
    startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
  }
  
  // 开启定时器
  function startTimer (pendingFunc, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  
  // 清除定时器
  function clearTimer (timerId) {
    if (useRAF) {
      return window.cancelAnimationFrame(timerId)
    }
    clearTimeout(timerId)
  }
  
  function debounced (...args) {
    lastThis = this
    lastArgs = args
    
    const isInvoking = shouldInvoke()
    
    if (isInvoking) {
      if (timer === undefined) {
        leadingEdge()
      }
    }
    
    clearTimer(timer)
    timer = startTimer(invokeFunc, wait)
  }
  
  return debounced
}
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

此时 ,函数并不完善,对于最后一次是否需要执行,没有做处理,也有一些其他的小问题,我们对它进行一个修复及处理

对于 debounce 来说,一开始先创建一个 定时器,只要 函数一直触发,到时间就什么都不做,再开一个定时器,到最终只会开一个定时器,保留最后一次

function debounce(func, wait) {
  
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发

  let timer,
      lastThis, // 返回函数的 this
      lastArgs, // 返回函数的参数
      result, // 最后的返回结果
      lastCallTime; // 最后调用的时间
  
  // 执行函数
  function invokeFunc () {
    let args = lastArgs
    let thisArg = lastThis
    
    result = func.apply(thisArg, args)
    lastArgs = lastThis = undefined
    return result
  }
  
  function shouldInvoke (time) {
    // 现在 shouldInvoke 会传入 时间,那么首先需要拿到时差,也就是当前的时间减去上一次的时间
    const timeSinceLastCall = time - lastCallTime
    // 拿到时间差后进行判断,如果 大于了 wait ,那么 func 也应该执行
    return lastCallTime === undefined || timeSinceLastCall >= wait
  }
  
  // 是否第一次执行
  function leadingEdge () {
    if(leading) invokeFunc() // 如果需要则执行函数
    timer = startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
  }
  
  function trailingEdge () {
    timer = undefined
    if (trailing) {
      invokeFunc()
    }
  }
  
  // 计算差值
  function remainingWait () {
    return wait - (now - lastCallTime)
  }
  
  function timerExpired () { // 当定时器到期了,是否需要执行函数
    const now = +new Date
    if (shouldInvoke(now)) { // 如果需要调用
      // 触发结束的方法
      return trailingEdge()
    }
    // 如果不满足触发条件,那么就再开一个定时器
    timer = startTimer(timerExpired, remainingWait(now))
  }
  
  // 开启定时器
  function startTimer (pendingFunc, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  
  // 清除定时器
  function clearTimer (timerId) {
    if (useRAF) {
      return window.cancelAnimationFrame(timerId)
    }
    clearTimeout(timerId)
  }
  
  function debounced (...args) {
    lastThis = this
    lastArgs = args
    const now = +new Date
    
    const isInvoking = shouldInvoke(now)
    
    lastCallTime = now
    if (isInvoking) {
      if (timer === undefined) {
        leadingEdge()
      }
    }
  }
  
  return debounced
}
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

至此 基本的 debounce 就实现了,接下来就要实现 throttle 相关的东西

在 throttle 中,也就是说 执行到了一定时间后,就会触发函数, 那么就需要一个 maxWait 参数

function debounce(func, wait, options = {}) {
  let maxWait
  if ('maxWait' in options) maxWait = options.maxWait
    
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }

  // 只有在 wait 假值,但是不是 0, 并且浏览器支持的情况下 使用 requestAnimationFrame
  const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
  
  wait = +wait || 0
  
  let leading = true // 第一次进入时触发
  let trailing = true // 最后一次也要触发

  let timer,
      lastThis, // 返回函数的 this
      lastArgs, // 返回函数的参数
      lastCallTime; // 最后调用的时间
  
  let lastInvokeTime = 0
  
  // 执行函数
  function invokeFunc (time) {
    let args = lastArgs
    let thisArg = lastThis
    
    lastInvokeTime = time
    
    func.apply(thisArg, args)
    lastArgs = lastThis = undefined
  }
  
  function shouldInvoke (time) {
    const timeSinceLastCall = time - lastCallTime
    
    // 得到当前时间和上一次调用时间的时间差
    const timeSinceLastInvoke = time - lastInvokeTime
    
    // 如果当前时差,大于了 throttle 时间,就执行
    return lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastInvoke >= maxWait
  }
  
  // 是否第一次执行
  function leadingEdge (time) { 
    lastInvokeTime = time
    if(leading) invokeFunc(time) // 如果需要则执行函数
    timer = startTimer() // 开启一个定时器, 看下一次定时器是否到了,是否需要执行 func
  }
  
  function trailingEdge (time) {
    timer = undefined
    if (trailing) {
      invokeFunc(time)
    }
  }
  
  // 计算差值
  function remainingWait () {
    return wait - (now - lastCallTime)
  }
  
  function timerExpired () { // 当定时器到期了,是否需要执行函数
    const now = +new Date
    if (shouldInvoke(now)) { // 如果需要调用
      // 触发结束的方法
      return trailingEdge(now)
    }
    // 如果不满足触发条件,那么就再开一个定时器
    timer = startTimer(timerExpired, remainingWait(now))
  }
  
  // 开启定时器
  function startTimer (pendingFunc, wait) {
    if (useRAF) {
      window.cancelAnimationFrame(timerId)
      return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
  }
  
  // 清除定时器
  function clearTimer (timerId) {
    if (useRAF) {
      return window.cancelAnimationFrame(timerId)
    }
    clearTimeout(timerId)
  }
  
  function debounced (...args) {
    lastThis = this
    lastArgs = args
    const now = +new Date
    
    const isInvoking = shouldInvoke(now)
    
    lastCallTime = now
    if (isInvoking) {
      if (timer === undefined) {
        leadingEdge(now)
      }
    }
  }
  
  return debounced
}
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
// throttle
function throttle (func, wait) {
  return debounce(func, wait, {
    maxWait: wait
  })
}
1
2
3
4
5
6

至此也就实现了 throttle 和 debounce 函数,在 lodash 源码中还对于参数的合法化做了严格的处理,对了 开始 和 结束是否执行也是通过参数传入的,具体的可以查看 lodash 源码 或者 查看我写的 lodash 源码分析 debounce (opens new window)

编辑 (opens new window)
#JS
上次更新: 2021/03/26 19:21:56
跨域及解决方案
从输入 URL 到页面展示,这中间发生了什么?

← 跨域及解决方案 从输入 URL 到页面展示,这中间发生了什么?→

最近更新
01
a标签下载限制
08-08
02
必应每日一图
05-27
03
小球碰壁反弹动画
05-26
更多文章>
Theme by Vdoing | Copyright © 2021-2023 Himawari | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式