【手写前端面试题01】防抖和节流

《手写防抖和节流:从“打工人”到“时间管理大师”》


一、本质理解(别被术语吓到)

防抖(debounce)是什么?

玩游戏看60秒广告只需要完整看一次就行了,退出去一次重新60秒,别一直退

核心思想: 在事件被触发后,等待一段时间,如果这段时间内没有再次触发,才执行。


节流(throttle)又是什么?

闪现CD:不管多急,都要等冷却好才能再次使用

核心思想: 多次触发 → 只按固定频率执行, 在一定时间内只允许执行一次操作,限制频率。


二、什么时候用防抖?什么时候用节流?

场景 使用
搜索框输入 ✅ 防抖(防止频繁请求)
窗口 resize ✅ 防抖或节流(根据需求)
页面滚动监听 ✅ 节流(避免频繁触发)
表单重复提交 ✅ 节流(防止多次点击)

三、核心思路(画个图我都能懂)

防抖函数逻辑图:

用户疯狂点击退出按钮
     ↓
启动定时器,广告倒计时500ms(广告要5秒钟看完)
     ↓
如果在倒计时内又点了一次(中途退出)
     ↓
清除上一个定时器,重新开始倒计时(重新看5秒广告)
     ↓
直到最后500ms内没人点了(不敢退了)
     ↓
终于执行真正的函数(看完广告,开始免费听歌)

节流函数逻辑图:

第一次点击 → 执行函数,记录当前时间
     ↓
接下来的点击 → 判断距离上次执行是否超过指定间隔
     ↓
没超过 → 不执行
     ↓
超过了 → 再执行一次,并更新时间

四、关键点(面试官最爱问)

特性 防抖 debounce 节流 throttle
是否立即执行 ❌ 默认不会,可加 immediate 参数控制 ✅ 可以设置首次立即执行
定时器数量 1 个 1 个
是否有返回值 否(除非封装 Promise)
是否需要清除定时器 ✅ 是 ✅ 是
常见应用场景 输入搜索、窗口调整 滚动加载、拖拽、高频点击

五、完整代码 + 注释(直接抄也能过面试)

防抖函数实现(debounce)

// 防抖函数,用于限制函数的执行频率
function debounce(fn, delay = 300, immediate = false) {
  // 定时器变量,用于存储setTimeout的返回值
  let timer = null;
  // 标记函数是否已立即执行过(仅在immediate为true时有效)
  let called = false;

  // 返回一个新的函数,该函数会包裹原始函数fn
  return function (...args) {
    // 保存当前函数的this上下文,确保在回调中能正确使用
    const context = this;

    // 如果存在未执行的定时器,则清除它(取消上一次的延迟执行)
    if (timer) clearTimeout(timer);

    // 如果配置了立即执行且当前尚未执行过
    if (immediate && !called) {
      // 立即执行原始函数,并绑定正确的this和参数
      fn.apply(context, args);
      // 标记为已执行
      called = true;
      return; // 直接返回,避免后续的定时器设置
    }

    // 设置新的定时器,延迟执行原始函数
    timer = setTimeout(() => {
      // 延迟执行原始函数,绑定正确的this和参数
      fn.apply(context, args);
      // 重置执行标记(允许下次再次立即执行)
      called = false;
    }, delay);
  };
}

关键点说明:

  1. 防抖核心逻辑:每次调用都会清除之前的定时器,确保只有在停止调用后的delay毫秒后才会执行函数。
  2. 立即执行模式immediate: true):
    • 第一次调用会立即执行函数
    • 后续在delay内的调用会被防抖
    • 执行后会重置called标记,允许下次再次立即执行
  3. 参数处理:使用剩余参数...args收集所有传入参数,并通过apply确保参数正确传递。
  4. this绑定:通过const context = this保存调用时的this值,确保回调中的this与原始调用上下文一致。
使用示例:
<input type="text" id="search" placeholder="输入内容搜索">
const input = document.getElementById('search');
input.addEventListener('input', debounce((e) => {
  console.log('发送请求:', e.target.value);
}, 500));

⏱️ 节流函数实现(throttle)

// 节流函数,用于限制函数的执行频率,确保在指定时间间隔内最多执行一次
function throttle(fn, delay = 300, leading = true, trailing = true) {
  // 记录上次执行的时间戳
  let lastTime = 0;
  // 定时器变量,用于存储setTimeout的返回值
  let timer = null;
  // 保存函数执行时的this上下文
  let context;
  // 保存函数执行时的参数
  let args;

  // 延迟执行的回调函数
  const later = () => {
    // 如果允许尾部执行(trailing为true)
    if (trailing) {
      // 更新最后执行时间
      lastTime = Date.now();
      // 清除定时器引用
      timer = null;
      // 执行原始函数
      fn.apply(context, args);
    }
  };

  // 返回包裹函数
  return function wrapper(...params) {
    // 保存当前函数的this上下文
    context = this;
    // 保存当前函数的参数
    args = params;
    // 获取当前时间戳
    const now = Date.now();

    // 如果从未执行过且不允许头部执行(leading为false)
    if (!lastTime && !leading) {
      // 设置初始执行时间为当前时间
      lastTime = now;
    }

    // 计算距离上次执行剩余的时间
    const remaining = delay - (now - lastTime);

    // 如果剩余时间<=0(可以立即执行)或者首次执行
    if (remaining <= 0 || remaining > delay) {
      // 如果存在未执行的定时器,则清除它
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      // 更新最后执行时间
      lastTime = now;
      // 执行原始函数
      fn.apply(context, args);
    } 
    // 如果允许尾部执行且没有定时器正在等待
    else if (!timer && trailing) {
      // 设置定时器,在剩余时间后执行later函数
      timer = setTimeout(later, remaining);
    }
  };
}

关键点说明:

  1. 节流核心逻辑:确保函数在指定时间间隔内最多执行一次。
  2. 两种执行模式
    • 头部执行leading: true):立即执行第一次调用
    • 尾部执行trailing: true):在停止调用后的延迟时间结束时执行
  3. 参数处理:通过闭包保存contextargs,确保在回调中能正确使用调用时的上下文和参数。
  4. 定时器管理:使用timer变量跟踪未执行的定时器,确保不会重复设置定时器。
  5. 边界情况处理:处理首次调用时leading为false的情况,以及剩余时间大于delay的特殊情况。

使用示例:

<div style="height: 2000px;">滚动页面看控制台div>
window.addEventListener('scroll', throttle(() => {
  console.log('页面正在滚动...');
}, 1000));

六、面试官灵魂拷问(附标准答案)

Q1:防抖函数为什么不能用 setInterval

A:因为 setInterval 是周期性执行,而防抖的关键在于“每次触发都要重新计时”。


Q2:如何让防抖函数在第一次就执行?

A:添加 immediate 参数,在第一次调用时立即执行,并在之后的触发中忽略,直到冷却期结束。


Q3:节流函数可以同时使用首次执行和末尾执行吗?

A:当然可以,通过 leadingtrailing 控制即可。


Q4:这两个函数能封装成 Vue 指令或者 React Hook 吗?

A:当然可以,比如:

  • Vue:自定义指令 v-debounce / v-throttle
  • React:封装成 useDebounce / useThrottle Hook

七、幽默吐槽

“我以前写了个输入框搜索功能,没加防抖,结果服务器直接宕机了,现在想想还觉得对不起运维。”

“节流就像我的工资,一个月只能发一次,不管你干了多少活。”

“防抖和节流就像是 JavaScript 中的时间管理大师,它们让你的代码不再‘焦虑’,也不再‘暴躁’。”


八、总结一句话

防抖(debounce):冷静一下,别急着执行。
节流(throttle):给我规矩点,不能太频繁。


九、拓展方向(进阶加分项)

技术方向 推荐做法
Vue 方向 写成自定义指令,如 v-debounce="search"
React 方向 写成 Hook,如 useDebounce(searchTerm, 500)
TypeScript 加类型注解,提升健壮性
性能优化 结合 requestIdleCallback、requestAnimationFrame

你可能感兴趣的:(javascript,前端,javascript,学习,防抖,节流)