出处:掘金

原作者:金泽宸


为什么要掌握防抖与节流?

它们是前端性能优化的“老三样”之一,常用于:

  • 防止频繁触发:输入框搜索、窗口 resize、滚动监听
  • 面试必问场景题:你怎么优化滚动事件监听?

概念区别一张图看懂

特性防抖(debounce)节流(throttle)
执行时机停止一段时间后执行一次固定时间间隔内最多执行一次
场景搜索框输入、按钮防重复提交页面滚动、resize、mousemove

函数防抖(debounce)

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

/**
 * @description: 函数防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
 * @param {function} func 需要防抖的函数
 * @param {number=500} delay 延迟时间(单位为毫秒),默认 500
 * @return {function}
 */
function debounce(func, delay = 500) {
  let timer = null
  return function(...args) {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
      timer = null
    }, delay)
  }
}

使用示例:

window.addEventListener('resize', debounce(() => {
  console.log('窗口变化');
}, 500));

函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

时间戳

/**
 * @description: 函数节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
 * @param {function} func 需要节流的函数
 * @param {number=500} wait 等待时间(单位为毫秒),默认 500
 * @return {function}
 */
function throttle(func, wait = 500) {
  let last = 0
  return function(...args) {
    const now = Date.now()
    if (now - last < wait) return
    last = now
    func.apply(this, args)
  }
}
  • 第一次立即执行
  • 无法保证最后一次是否执行

定时器

/**
 * @description: 函数节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
 * @param {function} func 需要节流的函数
 * @param {number=500} wait 等待时间(单位为毫秒),默认 500
 * @return {function}
 */
function throttle(func, wait = 500) {
  let timer = null
  return function(...args) {
    if (timer) return
    timer = setTimeout(() => {
      func.apply(this, args)
      timer = null
    }, wait)
  }
}
  • 第一次延迟执行
  • 最后一次延迟执行

时间戳 + 定时器

/**
 * @description: 函数节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
 * @param {function} func 需要节流的函数
 * @param {number=500} wait 等待时间(单位为毫秒),默认 500
 * @return {function}
 */
function throttle(func, wait = 500) {
	let timer = null
	let start = 0
	return function(...args) {
		let now = Date.now()
		let remainning = wait - (now - start) // 剩余时间
		timer && clearTimeout(timer)
		if (remainning <= 0) { // 没有剩余时间,需要立即执行
			func.apply(this, args)
			start = Date.now()
		} else { // 有剩余时间,剩余时间结束后执行
			timer = setTimeout(() => {
              func.apply(this, args)
              timer = null
            }, remainning)
		}
	}
}
  • 第一次立即执行
  • 最后一次延迟执行

使用场景

函数防抖

inputresize 等连续的事件,只需触发一次回调:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小 resize。只需窗口调整完成后,计算窗口大小。防止重复渲染

函数节流

scroll 等间隔一段时间执行一次回调:

  • 滚动加载,加载更多或滚到底部监听
  • 高频点击提交,表单重复提交

题目

常见面试陷阱

问题答案
防抖能否立即执行?可以,加 immediate 参数
节流 trailing 和 leading 如何实现?控制时间戳/定时器配合
节流和防抖能否合并?理论上不建议,但可以写工具库支持模式切换
thisarguments 会丢失吗?是的,需用 apply(this, args) 绑定上下文

延伸思考(面试加分)

  • lodash.debouncelodash.throttle 内部实现有何异同?
  • 如何封装一个 Vue/React 通用指令或 Hook 实现?