节流与防抖

524 阅读5分钟

前言

节流防抖是前端老生常谈的一个话题,这里我们简单回顾下概念:


当某个事件被频繁的触发时(比如输入滚动):

节流(throttle):会每隔一段时间执行一次

防抖(debounce):会只执行最后一次


这里我们以 input 事件为例:

const container = document.querySelector('#container')

function handleInput(event) {
  const { value } = event.target

  const div = document.createElement('div')
  container.appendChild(div).innerHTML = value
}

每输入一次就执行一次

可以看到在默认情况下,用户每输入一次就触发一次,很不环保。接下来我们实现节流与防抖函数,看看通过节流防抖处理后是什么样的。

实现节流

节流函数就就像红绿灯一样,当红灯时,不管有多少车辆排队,都需要等待红灯结束才能通行。

根据红绿灯的启发,我们得出思路:

当事件被触发时,开始计时,计时结束前不管触发了多少次,都不执行,直到计时结束后再去执行。

思路有了,方法就不难实现。

为了不影响原本的业务逻辑,原本的事件处理函数不变,只需要将其使用 throttle 函数包裹一下,再返回一个处理后的新的函数即可:

const throttledInput = throttle(handleInput)

接下来开始实现:

首先声明 throttle 函数,并通过参数 func 接收原本的函数:

function throttle(func) {
  // 提前声明一个变量,用于保存定时器
  let timer = null
  // throttle 需要返回一个新的函数,这个函数内部其实还是 func,区别就是等待一段时间后才会执行
  return function () {
    // 如果定时器还在运行,说明已经在等待中了,稍后就会执行了,所以不用再往下执行了
    if (timer) return
    // 否则,设置定时器,等待 500 毫秒后执行 func 函数
    timer = setTimeout(() => {
      // 并将其参数原封不动的还给 func,并执行 func 函数,参数其实就是每个事件都有的 event,我们不能因为做了节流防抖就把 event 数据给丢了
      func.apply(this, arguments)
      // 执行后就不用等待了,可以把定时器清空了,用于后续继续处理新的事件
      timer = null
    }, 500)
  }
}

在上面的代码中,我们主要创建了一个 timeout,当事件被触发时,开始计时,计时结束后执行事件处理函数。由于 timeout 可能已经在运行了,所以还要提前检查其是否在运行,如果在运行,就不用管他了,return 即可。

测试一下,为 input 事件添加节流函数:

input.addEventListener("input", throttledInput)

运行效果:

使用了节流函数,每过一段时间执行一次

还没完,由于 throttle 是一个通用函数,每个业务的等待时间可能不一样,为了使 throttle 更加灵活,需要将等待时间可以设置为一个参数,但不是必传参数,这里默认设置为 500,大家可以自行调整:

/**
 * @description 节流函数
 * @param {Function} func 需要节流的函数
 * @param {Number} delay 节流的时间
 * @returns {Function} 返回节流后的函数
 */
function throttle(func, delay = 500) {
  let timer = null
  return function () {
    if (timer) return
    timer = setTimeout(() => {
      func.apply(this, arguments)
      timer = null
    }, delay)
  }
}

实现防抖

就像文章开头说的,「防抖」就是只执行最后一次,如何判断是否为最后一次,那就要定义「最后」的时间范围,不然直到世界毁灭前都不算最后一次。

那么就设定一个等待时间吧,在这个等待时间结束前,都不算最后一次。如果等待时间内被重新触发就重新等待,直到不被触发,才算最后一次,才去执行传入的函数。

根据这个思路,我们就可以很轻松的实现 debounce 方法。首先明确使用方法,同样为了不改变原本的事件处理方法,我们使用 debounce 包裹一层即可:

const debouncedInput = debounce(handleInput)

将事件处理函数设为第一个参数 func,和上面的 throttle 一样,为了灵活性,将等待时间设为第二个参数 delay

function debounce(func, delay = 500) {
  // 创建一个定时器
  let timer
  // 返回一个包裹了 func 的新的函数,作用就是在等待结束后才会执行 func
  return function () {
    // 如果定时器还在运行,则清除定时器,重新等待下一个,直到在 delay 毫秒内等不到为止
    timer && clearTimeout(timer)
    // 等待 delay 毫秒后如果没被清除,就不等了,func 就会被执行了
    timer = setTimeout(() => {
      // 执行 func
      func.apply(this, arguments)
    }, delay)
  }
}

在上面的代码中,我们创建了一个 timeout,计时结束后会执行用户传入的函数。当事件被触发时,会首先检查timeout 是否在运行,如果在运行,则清除,重新等待,一直触发就会一直重新等待,不停的触发说明时间还没结束,所以不需要执行,直到 delay 毫秒(默认 500)后等不到才会执行。

为 input 事件添加防抖函数:

input.addEventListener("input", debouncedInput)

运行效果:

使用了防抖函数,停止输入后再去执行

最终代码:

/**
 * @description 防抖函数
 * @param {Function} func 需要防抖的函数
 * @param {Number} delay 防抖的时间
 * @returns {Function} 返回防抖后的函数
 */
function debounce(func, delay = 500) {
  let timer
  return function () {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

apply

上面的代码中使用了 apply,它是 Function 原型上的一个方法,作用是将一个函数的作用域设置为某个对象,并将参数传入函数。

有关 apply 的详细介绍,可以点击 这里 查看。