返回首页

19. 说说你对防抖和节流的理解?应用场景?

问题解析

防抖(debounce)和节流(throttle)是前端性能优化的重要手段,用于限制高频触发事件的执行次数。理解它们的原理、区别以及应用场景,对于提升用户体验和页面性能至关重要。

核心概念

什么是防抖和节流

本质上是优化高频率执行代码的一种手段。如:浏览器的resize、scroll、keypress、mousemove等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。

防抖(Debounce):n秒后在执行该事件,若在n秒内被重复触发,则重新计时。

节流(Throttle):n秒内只运行一次,若在n秒内重复触发,只有一次生效。

经典比喻

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。

假设电梯有两种运行策略debounce和throttle,超时设定为15秒,不考虑容量限制:

  • 节流:电梯第一个人进来后,15秒后准时运送一次
  • 防抖:电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送

详细解答

节流(Throttle)的实现

时间戳写法

function throttled1(fn, delay = 500) {
  let oldtime = Date.now();

  return function (...args) {
    let newtime = Date.now();
    if (newtime - oldtime >= delay) {
      fn.apply(null, args);
      oldtime = Date.now();
    }
  };
}

// 特点:事件会立即执行,停止触发后没有办法再次执行

定时器写法

function throttled2(fn, delay = 500) {
  let timer = null;

  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}

// 特点:delay毫秒后第一次执行,事件停止触发后依然会再一次执行

结合版(更精确)

function throttled(fn, delay) {
  let timer = null;
  let starttime = Date.now();

  return function () {
    let curTime = Date.now(); // 当前时间
    let remaining = delay - (curTime - starttime); // 剩余时间
    let context = this;
    let args = arguments;

    clearTimeout(timer);

    if (remaining <= 0) {
      fn.apply(context, args);
      starttime = Date.now();
    } else {
      timer = setTimeout(fn, remaining);
    }
  };
}

防抖(Debounce)的实现

基础版

function debounce(func, wait) {
  let timeout;

  return function () {
    let args = arguments; // 拿到event对象
    let context = this; // 保存this指向

    clearTimeout(timeout);

    timeout = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

立即执行版

function debounce(func, wait, immediate) {
  let timeout;

  return function () {
    let context = this;
    let args = arguments;

    if (timeout) clearTimeout(timeout);

    if (immediate) {
      let callNow = !timeout; // 第一次会立即执行

      timeout = setTimeout(() => {
        timeout = null;
      }, wait);

      if (callNow) func.apply(context, args);
    } else {
      timeout = setTimeout(() => {
        func.apply(context, args);
      }, wait);
    }
  };
}

防抖与节流的区别

特性 防抖(Debounce) 节流(Throttle)
触发时机 停止触发后执行 按固定频率执行
重复触发 重新计时 忽略,保持原频率
执行次数 最少执行一次(可能不执行) 固定频率执行
适用场景 搜索框输入、窗口调整 滚动加载、按钮点击

深入理解

使用场景分析

防抖的应用场景

// 1. 搜索框实时搜索
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
  // 用户停止输入300ms后才发送请求
  fetchSearchResults(e.target.value);
}, 300));

// 2. 窗口resize
window.addEventListener('resize', debounce(function() {
  // 窗口调整停止后才重新计算布局
  recalculateLayout();
}, 250));

// 3. 表单验证
const emailInput = document.getElementById('email');
emailInput.addEventListener('input', debounce(function(e) {
  // 用户停止输入后才验证
  validateEmail(e.target.value);
}, 500));

节流的应用场景

// 1. 滚动加载更多
window.addEventListener('scroll', throttle(function() {
  // 每200ms检查一次是否需要加载更多
  if (isNearBottom()) {
    loadMoreData();
  }
}, 200));

// 2. 按钮防重复点击
const submitBtn = document.getElementById('submit');
submitBtn.addEventListener('click', throttle(function() {
  // 1秒内只能提交一次
  submitForm();
}, 1000));

// 3. 游戏射击频率控制
const fireBtn = document.getElementById('fire');
fireBtn.addEventListener('click', throttle(function() {
  // 控制射击间隔
  fireBullet();
}, 200));

// 4. 鼠标跟随(降低频率)
document.addEventListener('mousemove', throttle(function(e) {
  // 降低更新频率,减少计算
  updateTooltipPosition(e.clientX, e.clientY);
}, 50));

结合Promise的防抖

function debounceWithPromise(func, wait) {
  let timeout;

  return function (...args) {
    clearTimeout(timeout);

    return new Promise((resolve) => {
      timeout = setTimeout(() => {
        resolve(func.apply(this, args));
      }, wait);
    });
  };
}

// 使用
const debouncedSearch = debounceWithPromise(searchAPI, 300);

searchInput.addEventListener('input', async (e) => {
  try {
    const results = await debouncedSearch(e.target.value);
    displayResults(results);
  } catch (error) {
    console.error(error);
  }
});

最佳实践

1. 使用Lodash/Underscore

// 实际项目中推荐使用成熟的库
import { debounce, throttle } from 'lodash';

// 防抖
const debouncedFn = debounce(myFunction, 300);

// 节流
const throttledFn = throttle(myFunction, 300);

// 选项配置
const debouncedFn = debounce(myFunction, 300, {
  leading: true,  // 立即执行
  trailing: false // 不执行尾部
});

2. React中的使用

import { useEffect, useCallback } from 'react';
import { debounce, throttle } from 'lodash';

// 防抖
function SearchComponent() {
  const debouncedSearch = useCallback(
    debounce((query) => {
      fetchResults(query);
    }, 300),
    []
  );

  useEffect(() => {
    return () => {
      debouncedSearch.cancel(); // 组件卸载时取消
    };
  }, [debouncedSearch]);

  return <input onChange={(e) => debouncedSearch(e.target.value)} />;
}

// 节流
function ScrollComponent() {
  const throttledScroll = useCallback(
    throttle(() => {
      handleScroll();
    }, 200),
    []
  );

  useEffect(() => {
    window.addEventListener('scroll', throttledScroll);
    return () => {
      window.removeEventListener('scroll', throttledScroll);
      throttledScroll.cancel();
    };
  }, [throttledScroll]);

  return <div>...</div>;
}

3. Vue中的使用

// 自定义指令
Vue.directive('debounce', {
  inserted(el, binding) {
    const { value, arg = 300 } = binding;
    el._debounceHandler = debounce(value, parseInt(arg));
    el.addEventListener('input', el._debounceHandler);
  },
  unbind(el) {
    el.removeEventListener('input', el._debounceHandler);
  }
});

// 使用
// <input v-debounce:500="handleInput" />

面试要点

  1. 核心区别:防抖是延迟执行,节流是固定频率执行
  2. 实现原理:防抖使用setTimeout/clearTimeout,节流使用时间戳或定时器
  3. 应用场景:能够根据场景选择合适的方案
  4. 立即执行:了解leading和trailing选项
  5. 取消功能:实际应用中需要支持取消功能

经典面试题

Q:防抖和节流有什么区别? A:防抖是在事件停止触发后才执行,如果持续触发则一直延迟;节流是按照固定频率执行,无论触发多频繁,只在固定时间间隔执行一次。

Q:什么场景用防抖,什么场景用节流? A:搜索框输入、窗口resize用防抖,因为只需要最终结果;滚动加载、按钮点击用节流,因为需要持续响应但又要控制频率。

Q:如何实现一个带立即执行选项的防抖函数? A:通过判断immediate参数,第一次调用时立即执行,同时设置定时器在wait时间后重置状态,期间再次调用则重新计时。