【每日一面】手写防抖函数

基础问答

问:手写一个防抖函数

答:

typescript 复制代码
/**
 * 基础版防抖函数(非立即执行)
 * @param {Function} func - 需要防抖的目标函数
 * @param {number} delay - 等待时间(毫秒)
 * @returns {Function} - 防抖后的函数
 */
function debounce(func, delay) {
  let timer = null; // 用闭包存储定时器ID,确保多次调用共享同一个定时器

  // 返回防抖后的函数,接收目标函数的参数(...args)
  return function (...args) {
    // 1. 若已有定时器,先清除(重复触发时重新计时)
    if (timer) clearTimeout(timer);

    // 2. 重新设置定时器,等待delay后执行目标函数
    timer = setTimeout(() => {
      func.apply(this, args); // 用apply绑定this(确保目标函数this指向正确)
      timer = null; // 执行后清空定时器,避免内存泄漏
    }, delay);
  };
}

扩展延伸

防抖(Debounce)

  • 核心逻辑:当一个事件会被频繁的触发时,防抖函数不会频繁的执行,而是等待事件停止触发一段时间后才执行;如果在等待执行的过程中,事件再次被触发,则我们需要重新计算需要等待的时间。
  • 典型使用场景:搜索框输入联想(每次按键都会触发输入对应 Input 的事件,我们认为用户结束输入后,我们再进行联想,这个结束输入的判定规则就是,自上一次Input事件触发后的一段时间内,用户没有再触发Input事件,可视为结束输入,需要开始联想),实时输入校验等。

和防抖并列提及的是节流。

节流(Throttle)

  • 核心逻辑:当事件会被频繁触发的时候,节流函数只会按照固定的时间间隔执行,无论期间事件被处罚多少次,都只在每个时间的开头(或结束)执行一次。
  • 典型使用场景:滚动监听(一般用于加载数据判断,每隔一段时间判断一次当前位置,来判断需要加载的数据量),窗口resize事件(用于重新计算布局)

这里简单对比一下防抖和节流:

防抖 节流
执行时机 事件停止触发后,等待一段指定的时间 固定时间间隔执行,每个时间段内仅执行一次
重复触发的问题 重新计算等待的时间,执行会延迟 不影响,固定时间间隔执行
目标 解决冗余的执行 解决过度的执行
使用场景 搜索、校验 滚动加载

搜索输入的过程中,每次键入字符触发搜索,在没有防抖的情况下,仅仅只有最后一次的搜索(即用户输入完成)才是有效的,之前的这些,全部都是没有意义的,只会加重服务负担,即为 "冗余"

页面滚动过程中,滚动是持续触发的,在没有节流的情况下,每一次滚动都会有大量的计算过程(假设你的滚动事件是有计算操作的),计算阻塞主线程,会导致页面卡顿,无法正常滚动,即为 "过度" ,如果使用防抖,滚动事件的持续触发,会导致计算一直无法开始,俗称"不跟手"。

面试追问

  1. setTimeout 的延时并不准,有没有办法实现一个更精确的时间检测?

    有,使用时间戳 + requestFrameAnimation 实现。

  2. 页面滚动加载数据一般用什么?搜索框输入触发联想词,又用什么?

    滚动加载一般用节流,防抖需要等用户停止滚动才加载,可能会等很久,节流则是一到底部就加载,可以保证加载的及时性。

    搜索联想一般用防抖,因为用户的输入过程会频繁触发联想,但是只有用户停止输入时,触发的联想才是用户想要的、有效的。

  3. 我看你在防抖函数中,用了 apply 这是为啥?为啥不可以直接用 func ?

    主要是 this 指针的指向问题,防抖函数返回的是一个新的函数,假设现在设置的是 input.oninput = debounceSearch,这个 debounceSearch 中如果有 this,那么预期是要指向 input 标签,但是我们直接调用 func 的话,this 会指向 window 或 undefined,和预期不一致。

  4. 防抖函数中,如果目标函数有返回值,我们可以拿到吗?

    不行,即使返回目标函数结果也不行,因为他在 setTimeout 里面执行的,无法返回对应的执行结果。

  5. 但是我就需要这个返回结果,有没有办法?

    有,两种办法,一是将 setTimout 用 Promise 封装起来,setTimeout 的回调执行时,resolve 这个 Promise 就可以了,这样防抖函数就变成了一个异步的api,二是使用回调参数,在目标函数执行后,调用这个回调就可以了。

  6. 有没有遇到过防抖函数导致内存泄漏的情况?

    没有,但是防抖函数有内存泄漏的可能性,本质上是闭包写法产生的,编写代码的时候注意闭包的处理就可以了。