Featured image of post JS 经典题: 防抖与节流

JS 经典题: 防抖与节流

前言

在前端开发中,我们经常会遇到一些频繁的事件触发,比如说 resizescrollmousemovekeydown 等等这些事件,对于这些事件,我们一般会采取防抖和节流的方式来提高性能。

防抖

定义

防抖是指在一定时间内,无论触发多少次事件,都只执行一次事件处理函数。如果在设定的延迟时间内再次触发事件,那么会重新计算延迟时间。防抖常用于搜索框输入等场景,避免输入时的频繁请求。

示例代码

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

这是一份非常经典的防抖代码,它的原理很简单,就是在一定时间内,如果再次触发事件,就清除上一次的定时器,重新计算延迟时间。

不过在这份代码里面还存在着许多考点,比如说 this 的指向问题,以及 arguments 的使用问题。

this 指向

在上面的代码中,我们使用了箭头函数,这样就可以保证 this 指向正确,但是如果我们不使用箭头函数,而是使用普通函数,那么 this 就会指向 window

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// <button>submit</button>

function debounce(fn, delay) {
  let timer = null
  return function() {
    clearTimeout(timer)
    timer = setTimeout(function() {
      fn.apply(this, arguments)
    }, delay)
  }
}

function submit() {
  console.log(this)
}

const btn = document.querySelector('button')
btn.addEventListener('click', debounce(submit, 1000)) // 当触发点击事件时,控制台会打印 window 对象

这时候我们就需要使用 bind 来改变 this 的指向。

1
2
3
4
5
6
7
8
9
function debounce(fn, delay) {
  let timer = null
  return function() {
    clearTimeout(timer)
    timer = setTimeout(function() {
      fn.apply(this, arguments)
    }.bind(this), delay) // 使用 bind 来改变 this 的指向
  }
}

或者我们可以提前保存 this 的指向。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function debounce(fn, delay) {
  let timer = null
  return function() {
    const that = this // 使用 that 来保存 this 的指向
    clearTimeout(timer)
    timer = setTimeout(function() {
      fn.apply(that, arguments)
    }, delay)
  }
}

arguments

arguments 的问题和 this 的问题类似,如果我们使用箭头函数,那么 arguments 就会指向外层函数的 arguments,如果我们使用普通函数,那么 arguments 就会指向 setTimeout 里面的回调函数的 arguments

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// <button>submit</button>

function debounce(fn, delay) {
  let timer = null
  return function() {
    clearTimeout(timer)
    timer = setTimeout(function() {
      fn.apply(this, arguments)
    }, delay)
  }
}

function submit(e) {
  console.log(e)
}

const btn = document.querySelector('button')
btn.addEventListener('click', debounce(submit, 1000)) // 当触发点击事件时,控制台会打印 undefined,因为 setTimeout 里面的回调函数没有参数

我们可以简单的用箭头函数来解决这个问题,也可以将 arguments 暂存起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function debounce(fn, delay) {
  let timer = null
  return function() {
    const args = arguments // 暂存 arguments
    clearTimeout(timer)
    timer = setTimeout(function() {
      fn.apply(this, args)
    }, delay)
  }
}

节流

节流是指在一定时间内只允许事件处理函数执行一次。如果在设定的时间内再次触发事件,那么不会执行事件处理函数,直到下一个时间段。节流常用于滚动事件等场景,避免频繁触发导致的性能问题。

节流有两种实现方式,我们可以通过 时间戳 来实现,也可以通过 定时器 来实现。

时间戳

1
2
3
4
5
6
7
8
9
function throttle(fn, delay) {
  let prev = Date.now()
  return function() {
    if (Date.now() - prev >= delay) {
      fn.apply(this, arguments)
      prev = Date.now()
    }
  }
}

定时器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function throttle(fn, delay) {
  let timer = null
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, arguments)
        timer = null
      }, delay)
    }
  }
}

使用场景

防抖:

  • 搜索框输入,防止用户输入过程中频繁请求接口。
  • 按钮提交,防止用户多次点击提交按钮,重复提交表单。
  • 浏览器窗口缩放,只需要等待窗口调整完成后,再执行计算事件。

节流:

  • 滚动加载,滚动过程中,每隔一段时间触发一次事件,而不是在滚动过程中频繁触发事件。
  • 鼠标移动,在处理鼠标移动事件时,也可以使用节流来限制事件处理函数的执行频率,提高性能。