小白也能理解的JS 节流throttle与防抖debounce函数

1,260 阅读4分钟

image.png

一、函数节流:一定时间间隔内,第一次有效

是指规定一个时间间隔,在这个时间间隔内,只能有一次触发事件的回调函数执行,如果在时间间隔内事件被触发多次,则不会执行回调。
应用场景:可以将一些事件降低触发频率,比如懒加载时要监听计算滚动条的位置,点击抽奖、抢购

按照定义,咱们先不考虑多的,按小白的理解写一个节流函数,以 Vue 为例:

  1. 假设我们使用【节流函数】时,该函数是被包裹在我们给按钮绑定的 clickMe 方法里面的
  2. 声明一个事件状态变量 timer 、声明节流函数 throttle
  3. 节流函数接受两个参数,参数一是传递的本该执行的方法 cb ,参数二是时间间隔 delay
  4. 进入方法,如果 timer 为真,则 return ,不执接下来的逻辑
  5. 如果 timernull ,则开启一个定时器,并且赋值给 timer ,定时器的等候时间即为 delay 参数,定时器执行后,清除计时器,并且将 timer 恢复为初始值 null
  6. 最后执行传递的方法 cb
  7. 这样当我们第一次点击按钮时,timernull,执行计时器,执行 cb,之后在 1s 内再次点击的话,timer 为真,则不会执行之后的逻辑,1s 过后,再次点击,timernull,则又可以执行了
<template>
  <button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>

<script>
  // 节流
  // 2、全局声明一个事件状态变量、声明节流函数
  let timer = null
  // 3、两个参数:参数一是传递的方法,参数二是节流的时间间隔
  function throttle (cb, delay) {
    // 4、进入方法,如果timer为真,则return,不执接下来的逻辑
    if (timer) return
    // 5、否则则开启一个定时器,并且赋值给timer,定时器的等候时间即为delay参数,定时器执行后,清除计时器,并且将timer恢复为初始值null
    timer = setTimeout(() => {
      clearTimeout(timer)
      timer = null
    }, delay)
    // 执行传递的方法cb
    return cb()
  }
  export default {
    methods: {
      clickMe (name, name2) {
        // 1、假设我们使用【节流函数】时,是被包裹在我们给按钮绑定的clickMe 方法里面的
        throttle(() => {
          console.log('点了!', name)
          console.log('点了!', name2)
        }, 1000)
      }
    }
  }
</script>

但是,这种实现方式是有问题的,因为 timer 是被定义在【全局作用域】的,所以用闭包(有权访问另一个函数作用域变量的函数,称为闭包)的形式改进:

  1. 现在 throttle 直接绑定在 clickMe 上,
  2. 当前组件初始化的时候,clickMe 绑定的 throttle 会被执行一次,throttle 会将我们传递的cb 方法进行包装,再 return 一个新的方法给 clickMe
  3. 此时 timer 的作用域也被限制在了 throttle
<template>
  <button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>

<script>
// 节流
function throttle (cb, delay) {
  // 计时器
  let timer
  // 调用 throttle 后,会将 cb 进行包装、执行并返回给调用者
  return function () {
    // 如果计时器存在,则阻止用户操作
    if (timer) return
    // 否则开启一个计时器
    timer = setTimeout(() => {
      // 时间间隔结束,结束计时器
      clearTimeout(timer)
      // 将 timer 初始化
      timer = null
    }, delay)
    // 执行回调
    cb(...arguments)
  }
}
export default {
  methods: {
    // 现在throttle直接绑定在clickMe上,throttle 会 return一个新的方法
    clickMe: throttle((name, name2) => {
      console.log('点了!', name)
      console.log('点了!', name2)
      console.log(this) // undefined
    }, 1000)
  }
}
</script>

此处几个知识点:

  1. arguments 和 剩余参数
  • 在普通函数 function 中,要想获得所有参数对象,则是通过关键字 arguments 可以获取
  • 在箭头函数中,没有 arguments,则可以通过剩余参数 (...args) 获取 但是,我们现在方法中获取 this 的话,是获取不到的,所以我们在执行 cb(...arguments) 需要修改 cb 内部 this 指向,有三种方法:
  1. 修改 this 指向
  • cb.call(this, ...arguments)
  • cb.apply(this, arguments)
  • cb.bind(this)(...arguments) 这样,我们就能正确的获取 this 指向了。

最后我们对代码在进行一下优化,相较于计时器,我们可以用 Date.now() 来替代,性能会更好,以下就是完整代码:

<template>
  <button @click="clickMe('Jerry', 'Tom')">点我吧!</button>
</template>

<script>
// 节流
function throttle (cb, delay) {
  // 上次操作的时间
  let lastTime = 0
  return function () {
    const triggerTime = Date.now()
    // 当前操作时间 - 上次操作时间 > 时间间隔,才执行
    if (triggerTime - lastTime > delay) {
      cb.call(this, ...arguments)
      lastTime = triggerTime
    }
  }
}
export default {
  methods: {
    clickMe: throttle(function (name, name2) {
      console.log('点了!', name)
      console.log('点了!', name2)
      console.log('点了!', this)
    }, 1000)
  }
}
</script>

二、函数防抖:一定时间间隔内,最后一次有效

image.png 是指规定一个时间间隔,在事件被触发该时间间隔后再执行回调,如果在这个时间间隔内事件又被触发,则重新计时,不执行回调。
应用场景:搜索框(当用户输入内容后,n秒后没有再次输入,就搜索)、监听窗口放大缩小resize、滚动监听

function debounce(cb, delay = 500) {
  // 计时器
  let timer = null
  return function (...args) {
    // 如果计时器存在,则清除计时器,重新计时(如果用户一直在输入,则重置计时器)
    if (timer) clearTimeout(timer)
    // 开启计时器(delay时间后,用户没有输入,则出发搜索)
    timer = setTimeout(() => {
      // 到达时间间隔,执行回调
      cb.call(this, args)
    }, delay)
  }
}

使用:在 onScorll 中使用防抖

// 用debounce来包装scroll的回调
const scroll = debounce(() => {
  console.log("触发了滚动事件")
}, 1000)
document.addEventListener("scroll", scroll)

三、throttlePlus:使用 debounce 来优化 throttle

throttle 的问题是它太有耐心了,假设:

  • 首先我们将 delay 设置为 3s ,当用户第一次点击后,会执行 cb
  • 紧接着,用户狂点,但是并未等到 3s 这个时间节点到达之前,就停止了点击
  • 那么除了用户第一次的点击会执行 cb 之外,后面的这些点击都不会执行 cb
  • 这样就会导致用户的操作一直没有得到反馈,对这个页面产生“卡死”了的感觉。 所以,为了解决这样的一个问题,我们需要将 throttledebounce 结合起来,若用户在 3s 之前结束了操作,那么计时器到了 3s 这个时刻,就执行一次 cb
function throttlePlus (cb, delay) {
  // 上次操作的时间
  let lastTime = 0
  // 计时器
  let timer = null
  return function () {
    /** @不在时间间隔内 **/
    // 当前操作的时间
    const triggerTime = Date.now()
    // 如果 当前操作的时间 - 上次操作的时间 > 时间间隔,则执行 cb
    if (triggerTime - lastTime > delay) {
      lastTime = triggerTime
      cb.call(this, ...arguments)
    } else {
      /** @在时间间隔内 **/
      // 用户若一直操作,则重置计时器
      timer && clearTimeout(timer)
      // 设置定时器,到达我们设置的 delay 间隔,就给用户执行一次
      timer = setTimeout(() => {
        lastTime = triggerTime
        clearTimeout(timer)
        cb.call(this, ...arguments)
      }, delay)
    }
  }
}

使用:在 onScorll 中使用加强版 throttlePlus

// 用throttlePlus来包装scroll的回调
const scroll = throttlePlus(() => {
  console.log("触发了滚动事件")
}, 1000)
document.addEventListener("scroll", scroll)