前言
在前端开发中,我们经常会遇到一些频繁的事件触发,比如说 resize、scroll、mousemove、keydown 等等这些事件,对于这些事件,我们一般会采取防抖和节流的方式来提高性能。
防抖
定义
防抖是指在一定时间内,无论触发多少次事件,都只执行一次事件处理函数。如果在设定的延迟时间内再次触发事件,那么会重新计算延迟时间。防抖常用于搜索框输入等场景,避免输入时的频繁请求。
示例代码
| 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)
    }
  }
}
 | 
 
使用场景
防抖:
- 搜索框输入,防止用户输入过程中频繁请求接口。
- 按钮提交,防止用户多次点击提交按钮,重复提交表单。
- 浏览器窗口缩放,只需要等待窗口调整完成后,再执行计算事件。
节流:
- 滚动加载,滚动过程中,每隔一段时间触发一次事件,而不是在滚动过程中频繁触发事件。
- 鼠标移动,在处理鼠标移动事件时,也可以使用节流来限制事件处理函数的执行频率,提高性能。