# 理解节流与防抖函数

节流(throttle)函数,可以降低触发频率,每隔一段时间才去执行一次,常用于 resize, scroll 等触发非常频繁但同时需要及时响应的场景。

防抖(debounce)函数,可以防止用户频繁触发某一操作,当用户不再频繁触发时(比如间隔 300ms),才去执行那个操作,常用于input``onchange事件,表单提交submit等。

# 节流函数

# 无定时器版本

/**
 * 节流函数,降低触发频率,比如表单提交以及resize,scroll事件等
 * @param {Function} fn 回调函数
 * @param {number} interval 触发间隔
 */
const throttle = (fn, interval) => {
  let last = 0
  return (...args) => {
    let context = this
    let now = Date.now()
    // 节流函数在第一次执行的时候一定会触发一次
    if (now - last >= interval) {
      last = now
      fn.apply(context, args)
    }
  }
}

使用

const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)

throttle是一个高阶函数,传入的参数包含函数,且返回的也是一个函数。所以也自然形成了闭包。

上例中,使用了better_scroll接收了throttle返回的函数,在better_scroll函数内部持续引用这last的值,导致throttle内部的值last一直得不到销毁,所以形成了闭包。

疑问一:既然没有使用定时器,那还有保存this的必要吗?

个人觉得没必要,因为this在执行时上下文环境没有发生变化,即由代码let context = this;fn.apply(context, args);上下文环境没有发生变化,所以是没有必要的。

const throttle = (fn, interval) => {
  let last = 0
  return (...args) => {
    let now = Date.now()
    // 第一次一定会触发
    if (now - last >= interval) {
      last = now
      fn.apply(this, args)
    }
  }
}

疑问二: 有必要使用apply来保存this并传参吗,下面的代码和上面的代码有区别吗

const throttle = (fn, interval) => {
  let last = 0
  return (...args) => {
    let now = Date.now()
    // 第一次一定会触发
    if (now - last >= interval) {
      last = now
      fn(...args)
    }
  }
}

可以去绑定 this ,因为返回的函数有可能会使用 bind 改变 this ,执行的函数可以实现透传 this

疑问三:args 参数有必要吗?

看一下传 args 效果

const scrollMethod = (...args) => {
  console.log('触发滚动事件')
  console.log('参数是:', ...args)
}
const better_scroll = throttle(scrollMethod, 1000)
// document.addEventListener('scroll', better_scroll(5, 6, 7)) // 这样传的是函数的执行,只会执行一次
// 传递的是已经执行的函数
const better_scroll_params = better_scroll(5, 6, 7)
document.addEventListener('scroll', better_scroll) 

结果只在页面第一次加载的时候执行了该函数,多次滚动并没有触发better_scroll_params该函数,为什么呢?因为better_scroll_params函数是已经执行好了的函数,并不是函数的定义,所以也就只在第一次执行了一次。

从上面来看,args 参数确实是没必要的。但是有了 args 易于扩展

# 有定时器版本

定时器版本和无定时器版本主要区别:

  1. 定时器版本不会立即执行
  2. 定时器版本会有产生宏任务 setTimoutout
/**
 * 定时器版本节流函数
 * @param {Function} func
 * @param {number} wait
 */
const throttle = (func, wait) => {
  let timerId
  return function foo() {
    let context = this
    let args = arguments
    // 每隔一段时间触发一次,不需要清除定时器
    if (!timerId) {
      timerId = setTimeout(function() {
        timerId = null
        func.apply(context, args)
      }, wait)
    }
  }
}

以上面这个定时器版本,来看待非定时器版本的节流函数存在的几个问题。

  1. 保存 this 有必要吗?

可以保存,因为在setTimeout的第一个函数参数中,thiswindow,所以需要把this执行改为foo函数的this。但如果使用了箭头函数,就不用保存,但是个人觉得这里的环境并没有什么实际作用(因为我们不需要重新给 foo 绑定 this)

const throttle = (func, wait) => {
  let timerId
  return function foo(...args) {
    if (!timerId) {
      timerId = setTimeout(() => {
        timerId = null
        func.apply(this, args)
      }, wait)
    }
  }
}

const better_scroll = throttle(() => console.log('触发了滚动事件'), 1000)
document.addEventListener('scroll', better_scroll)
  1. 有必要使用 apply 吗?主要是为了传递,this,应该使用
  2. 有必要使用 args 吗?可以不需要

# 补充在 react 中遇到的问题

  1. 假如为window使用addEventListtener绑定了scroll事件,那么在组件卸载时使用removeEventlistener移除事件时,需要保证两个事件函数是同一个引用
  2. setTimeoutsetState 是同步的,正常情况下 setState 是异步的

# 防抖函数

/**
 * 防抖函数,频繁操作只会执行最后一次
 * @param {Function} func 回调函数
 * @param {number} delay 延迟
 */
export const debounce = (func, delay) => {
  let timer
  return function foo(...args) {
    // debugger;
    // console.log(this, 98989898);
    // let context = this
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      // 普通函数需要执行this func()直接执行this 是window
      // 箭头函数 this是静态语法 即与setTimeout所处环境的this一致,即与函数debounceReturnCb环境一致
      // apply第二个是数组
      // func.apply(context, args)
      func.apply(this, args)
      clearTimeout(timer)
    }, delay)
  }
}

# 参考

LastEditTime: 2023/2/19 15:38:37