《10分钟彻底搞懂防抖和节流!前端性能优化的核心技巧,面试必问!》

111 阅读4分钟

在前端开发中,像浏览器的 resize、scroll、keypress、mousemove 等高频事件触发时,会持续调用绑定的回调函数,这可能导致浏览器资源浪费、页面卡顿甚至性能下降。为优化此类场景的用户体验,防抖(Debounce)节流(Throttle) 是两种常用的频率控制手段,通过限制回调函数的执行次数,减少无效计算,提升应用性能。

防抖

在事件被触发后,等待一段时间再执行回调,如果在这段时间内事件又被触发,则重新计时。确保事件停止触发后只执行一次回调函数。

防抖常用场景

防抖常用于输入框实时搜索、窗口调整大小、按钮多次点击等场景,避免频繁触发导致的性能问题。

搜索框的实时搜索建议(用户停止输入后请求):防止用户输入过程中频繁发送请求。 窗口大小调整时的重绘操作:避免因窗口频繁调整而持续触发重排重绘,影响性能。 按钮防连击:确保按钮不会因为用户快速连续点击而多次触发同一动作。

防抖的实现方式

延迟执行的防抖函数

function debounce(fn, delay) {
    let timer = null;//初始化定时器
    return function(...args) {//收集所有参数到args数组
       //如果定时器已存在就清除它
        if (timer) clearTimeout(timer);
        // 设置新的定时器,在延迟delay毫秒后执行函数
        timer = setTimeout(()=> {
            fn.apply(this, args);//将参数数组都传递给原始函数fn
            timer=null; // 清除定时器,重置timer,允许下次触发
        }, delay);
    }
}

首先防抖代码中的参数fn是需要执行的函数,delay是延迟执行的时间。
上面防抖代码中的剩余参数和箭头函数是ES6中的新特性,剩余参数用于收集函数调用时的所有参数,打包成一个数组。箭头函数则用于简化函数的写法,并自动绑定当前上下文中的this值。下面是普通函数重写的防抖函数(依旧延迟执行)

function debounce(fn, delay) {
  let timer = null;
  
  return function() {
    const self = this; // 保存 this
    const args = Array.from(arguments); // 将 arguments 转为数组
    
    if (timer) clearTimeout(timer);
    
    timer = setTimeout(function() {
      fn.apply(self, args); // 使用保存的 this 和参数
    }, delay);
  };
}

立即执行的防抖函数

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

    return function(...args){
        //若定时器已存在就清除它
        if(timer) clearTimeout(timer);
        // 若是第一次调用或者定时器已经执行完毕,则立即执行函数
        const shouldCallNow=!timer;
        // 设置新的定时器,在延迟后充值状态
        timer=setTimeout(()=>{
            timer=null;
        },delay);
        // 立即执行函数
        if(shouldCallNow) fn.apply(this,args);

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

节流

节流就是指连续触发事件但是在n秒中只执行一次函数。节流会稀释函数的执行频率(控制最小触发间隙)。

节流的常用场景

节流常用在滚动加载、高频事件(mousemove、touchmove)监听等场景,避免因事件触发频率过高导致性能问题。

节流的实现方式

时间戳节流函数

function throttle(fn, limit) {
    //记录上次执行的时间戳
    let lastTime = 0;

    return function(...args) {
        //获取当前时间戳
        const now = Date.now();
        //判断是否大于等于时间间隔
        if (now - lastTime >= limit) {
            //执行函数并传递参数
            fn.apply(this, args);
            //更新时间戳
            lastTime = now;
    }
}

初始化上次执行时间戳为0,第一次调用时,时间间隔是0,所以一定会立即执行,执行之后更新时间戳,确保两次执行之间间隔limit毫秒,停止触发后不会再次执行。

定时器节流函数

function throttleTimer(fn,limit){
    let timer=null;

    return function(...args){
        // 定时器不存在就设置定时器
        if(!timer){
            timer=setTimeout(()=>{
            fn.apply(this,args);
            timer=null;
            },limit);
        }
    };
}

定时器节流函数首次调用会等待limit毫秒后执行,停止触发后会在limit毫秒后执行。如果在定时器等待期间多次触发,只有第一次会被记录,也就是说定时器节流函数的执行间隔不严格。

综合版本节流函数

function throttle(func, limit) {
  let lastExecTime = 0; // 记录上次执行时间
  let timer = null; // 记录定时器状态
  
  return function(...args) {
    const now = Date.now();
    const remaining = limit - (now - lastExecTime); // 计算距离下次执行还需的时间
    
    // 如果距离上次执行时间超过了限制时间,则立即执行函数
    if (remaining <= 0) {
      // 如果有等待执行的定时器,则清除它
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      
      func.apply(this, args); // 立即执行
      lastExecTime = now; // 更新上次执行时间
    } 
    // 否则设置定时器,在剩余时间后执行函数
    else if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args); // 延迟执行
        lastExecTime = Date.now(); // 更新上次执行时间
        timer = null; // 重置定时器状态
      }, remaining);
    }
  };
}

综合版本的节流函数结合了时间戳和定时器的优点,首次调用会立即执行,停止触发后会在剩余时间后执行,如果在定时器等待期间多次触发,未超过limit首次触发创建定时器后续触发被忽略,最终执行一次(首次触发的参数);超过limit,清除定时器,立即执行当前触发的函数,执行一次。