Skip to content

节流实现

节流(Throttle)是前端开发中常用的性能优化技术,它可以限制函数在一定时间内只能执行一次,避免在短时间内重复执行同一个函数,从而提高页面性能和用户体验。

核心概念

什么是节流?

节流是指在一定时间内,多次触发同一个函数,只执行一次。例如,在用户滚动页面时,我们可以使用节流技术,每 100ms 只执行一次滚动回调函数,而不是每次滚动都执行。

节流的应用场景

  • 滚动事件:滚动页面时,每 100ms 只执行一次回调函数
  • 鼠标移动:鼠标移动时,每 50ms 只执行一次回调函数
  • 游戏开发:游戏循环中,每帧只执行一次游戏逻辑
  • 按钮点击:防止用户快速点击按钮,每 1000ms 只执行一次点击

实现原理

节流的实现原理有两种:

  1. 时间戳版本:使用时间戳记录上次执行的时间,当当前时间与上次执行时间的差值大于等于等待时间时,执行函数
  2. 定时器版本:使用定时器,当定时器到期时,执行函数并重置定时器

实现代码

时间戳版本

javascript
function throttle(func, wait) {
  let previous = 0;
  return function() {
    const now = Date.now();
    const context = this;
    const args = arguments;
    if (now - previous >= wait) {
      func.apply(context, args);
      previous = now;
    }
  };
}

定时器版本

javascript
function throttle(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(context, args);
      }, wait);
    }
  };
}

优化版本(支持立即执行和尾部执行)

javascript
function throttle(func, wait, options = {}) {
  let timeout;
  let previous = 0;
  const { leading = true, trailing = true } = options;
  
  const later = function() {
    previous = leading === false ? 0 : Date.now();
    timeout = null;
    func.apply(this, arguments);
  };
  
  return function() {
    const now = Date.now();
    if (!previous && leading === false) {
      previous = now;
    }
    const remaining = wait - (now - previous);
    const context = this;
    const args = arguments;
    
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
    } else if (!timeout && trailing !== false) {
      timeout = setTimeout(later.bind(context, ...args), remaining);
    }
  };
}

优化版本(支持取消)

javascript
function throttle(func, wait, options = {}) {
  let timeout;
  let previous = 0;
  const { leading = true, trailing = true } = options;
  
  const later = function() {
    previous = leading === false ? 0 : Date.now();
    timeout = null;
    func.apply(this, arguments);
  };
  
  const throttled = function() {
    const now = Date.now();
    if (!previous && leading === false) {
      previous = now;
    }
    const remaining = wait - (now - previous);
    const context = this;
    const args = arguments;
    
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
    } else if (!timeout && trailing !== false) {
      timeout = setTimeout(later.bind(context, ...args), remaining);
    }
  };
  
  throttled.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
    previous = 0;
  };
  
  return throttled;
}

使用示例

滚动事件节流

javascript
function handleScroll() {
  console.log('滚动位置:', window.scrollY);
  // 执行滚动相关操作
}

// 使用节流,100ms内只执行一次滚动操作
const throttledScroll = throttle(handleScroll, 100);

window.addEventListener('scroll', throttledScroll);

鼠标移动节流

javascript
function handleMouseMove(e) {
  console.log('鼠标位置:', e.clientX, ',', e.clientY);
  // 执行鼠标移动相关操作
}

// 使用节流,50ms内只执行一次鼠标移动操作
const throttledMouseMove = throttle(handleMouseMove, 50);

window.addEventListener('mousemove', throttledMouseMove);

按钮点击节流

javascript
// HTML: <button id="click">点击</button>

const clickButton = document.getElementById('click');

function handleClick() {
  console.log('按钮点击');
  // 执行点击相关操作
}

// 使用节流,1000ms内只执行一次点击
const throttledClick = throttle(handleClick, 1000, { leading: true, trailing: false });

clickButton.addEventListener('click', throttledClick);

取消防节流

javascript
// 取消防节流
function cancelThrottle() {
  throttledScroll.cancel();
  console.log('节流已取消');
}

// HTML: <button id="cancel">取消节流</button>
document.getElementById('cancel').addEventListener('click', cancelThrottle);

性能优化

1. 使用 requestAnimationFrame 代替 setTimeout

对于需要频繁执行的动画或视觉效果,可以使用 requestAnimationFrame 代替 setTimeout,以获得更好的性能。

javascript
function throttleRAF(func) {
  let ticking = false;
  return function() {
    const context = this;
    const args = arguments;
    if (!ticking) {
      requestAnimationFrame(() => {
        func.apply(context, args);
        ticking = false;
      });
      ticking = true;
    }
  };
}

2. 使用闭包缓存参数

对于需要频繁调用的函数,可以使用闭包缓存参数,以减少函数调用的开销。

javascript
function throttleWithCache(func, wait) {
  let timeout;
  let previous = 0;
  let lastArgs;
  let lastThis;
  
  return function() {
    lastThis = this;
    lastArgs = arguments;
    const now = Date.now();
    
    if (now - previous >= wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      func.apply(lastThis, lastArgs);
      previous = now;
    } else if (!timeout) {
      timeout = setTimeout(() => {
        previous = now;
        timeout = null;
        func.apply(lastThis, lastArgs);
      }, wait - (now - previous));
    }
  };
}

面试常见问题

1. 节流和防抖的区别是什么?

答案示例: 节流和防抖都是前端开发中常用的性能优化技术,它们的区别在于:

  • 节流:在一定时间内,多次触发同一个函数,只执行一次。例如,滚动页面时,每 100ms 只执行一次回调函数。
  • 防抖:在一定时间内,多次触发同一个函数,只执行最后一次。例如,用户输入搜索关键词时,只在停止输入后才发送搜索请求。

2. 如何实现一个节流函数?

答案示例: 实现节流函数的核心原理有两种:

  1. 时间戳版本:使用时间戳记录上次执行的时间,当当前时间与上次执行时间的差值大于等于等待时间时,执行函数。
javascript
function throttle(func, wait) {
  let previous = 0;
  return function() {
    const now = Date.now();
    const context = this;
    const args = arguments;
    if (now - previous >= wait) {
      func.apply(context, args);
      previous = now;
    }
  };
}
  1. 定时器版本:使用定时器,当定时器到期时,执行函数并重置定时器。
javascript
function throttle(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    if (!timeout) {
      timeout = setTimeout(() => {
        timeout = null;
        func.apply(context, args);
      }, wait);
    }
  };
}

3. 节流函数的应用场景有哪些?

答案示例: 节流函数的应用场景包括:

  • 滚动事件:滚动页面时,每 100ms 只执行一次回调函数
  • 鼠标移动:鼠标移动时,每 50ms 只执行一次回调函数
  • 游戏开发:游戏循环中,每帧只执行一次游戏逻辑
  • 按钮点击:防止用户快速点击按钮,每 1000ms 只执行一次点击
  • 网络请求:限制网络请求的频率,避免过多的请求导致服务器压力过大

4. 如何优化节流函数的性能?

答案示例: 优化节流函数的性能可以从以下几个方面入手:

  1. 使用 requestAnimationFrame 代替 setTimeout:对于需要频繁执行的动画或视觉效果,可以使用 requestAnimationFrame 代替 setTimeout,以获得更好的性能。

  2. 使用闭包缓存参数:对于需要频繁调用的函数,可以使用闭包缓存参数,以减少函数调用的开销。

  3. 避免不必要的计算:在节流函数中,避免进行不必要的计算,只在必要时才执行函数。

  4. 使用 this 绑定:确保节流函数中的 this 指向正确,以避免出现上下文丢失的问题。

5. 如何取消防节流函数?

答案示例: 取消防节流函数的方法是清除定时器并重置时间戳,具体实现如下:

javascript
function throttle(func, wait, options = {}) {
  let timeout;
  let previous = 0;
  const { leading = true, trailing = true } = options;
  
  const later = function() {
    previous = leading === false ? 0 : Date.now();
    timeout = null;
    func.apply(this, arguments);
  };
  
  const throttled = function() {
    const now = Date.now();
    if (!previous && leading === false) {
      previous = now;
    }
    const remaining = wait - (now - previous);
    const context = this;
    const args = arguments;
    
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
    } else if (!timeout && trailing !== false) {
      timeout = setTimeout(later.bind(context, ...args), remaining);
    }
  };
  
  throttled.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
    previous = 0;
  };
  
  return throttled;
}

使用时,可以调用 throttled.cancel() 方法来取消防节流。

总结

节流是前端开发中常用的性能优化技术,它可以限制函数在一定时间内只能执行一次,避免在短时间内重复执行同一个函数,从而提高页面性能和用户体验。

实现节流函数的核心原理有两种:时间戳版本和定时器版本。时间戳版本使用时间戳记录上次执行的时间,当当前时间与上次执行时间的差值大于等于等待时间时,执行函数;定时器版本使用定时器,当定时器到期时,执行函数并重置定时器。

节流函数的应用场景包括滚动事件、鼠标移动、游戏开发、按钮点击等,它可以有效地减少不必要的函数调用,提高页面性能。

通过学习和使用节流函数,你将能够更好地优化前端代码,提高用户体验,为面试和工作做好准备。

好好学习,天天向上