Skip to content

节流

节流(Throttle)是一种常用的性能优化技术,用于减少函数的调用次数,提高代码的执行效率。它的核心思想是:在事件触发的过程中,每隔一段时间执行一次函数调用,无论期间触发了多少次事件。

节流的应用场景

节流函数在以下场景中特别有用:

  1. 滚动事件:当用户滚动页面时,不需要每次滚动都执行回调函数,而是每隔一段时间执行一次
  2. 鼠标移动事件:当用户移动鼠标时,不需要每次移动都执行回调函数,而是每隔一段时间执行一次
  3. 游戏中的动画效果:游戏中的动画效果需要每隔一段时间更新一次,而不是每次渲染帧都更新
  4. 网络请求:当用户频繁点击按钮发送网络请求时,不需要每次点击都发送请求,而是每隔一段时间发送一次

节流函数的实现

时间戳实现

使用时间戳实现节流函数的思路是:记录上次执行函数的时间戳,当事件触发时,计算当前时间戳与上次执行时间戳的差值,如果差值大于等于延迟时间,则执行函数,并更新上次执行时间戳。

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

定时器实现

使用定时器实现节流函数的思路是:在事件触发时,启动一个定时器,如果在定时器到期前又触发了事件,则忽略该事件。当定时器到期时,执行函数,并清除定时器。

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

时间戳和定时器结合实现

时间戳实现的节流函数在事件触发后立即执行,定时器实现的节流函数在事件触发后延迟执行。结合两者可以实现一个更加完善的节流函数,既可以在事件触发后立即执行,又可以在事件结束后执行一次。

javascript
function throttle(func, delay, options = {}) {
  let lastCall = 0;
  let timer = null;
  const { leading = true, trailing = true } = options;
  
  return function() {
    const context = this;
    const args = arguments;
    const now = Date.now();
    
    // 如果是第一次触发,并且不需要立即执行,则设置lastCall为当前时间
    if (!lastCall && !leading) {
      lastCall = now;
    }
    
    // 计算剩余时间
    const remaining = delay - (now - lastCall);
    
    // 如果剩余时间小于等于0,或者超过了延迟时间(可能是由于系统时间调整),则执行函数
    if (remaining <= 0 || remaining > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastCall = now;
      func.apply(context, args);
    } else if (!timer && trailing) {
      // 如果没有定时器,并且需要在事件结束后执行,则设置定时器
      timer = setTimeout(() => {
        lastCall = leading ? Date.now() : 0;
        timer = null;
        func.apply(context, args);
      }, remaining);
    }
  };
}

带取消功能的实现

有时候,我们希望能够取消节流函数的执行,这种情况下可以添加一个cancel方法。

javascript
function throttle(func, delay, options = {}) {
  let lastCall = 0;
  let timer = null;
  const { leading = true, trailing = true } = options;
  
  const throttled = function() {
    const context = this;
    const args = arguments;
    const now = Date.now();
    
    // 如果是第一次触发,并且不需要立即执行,则设置lastCall为当前时间
    if (!lastCall && !leading) {
      lastCall = now;
    }
    
    // 计算剩余时间
    const remaining = delay - (now - lastCall);
    
    // 如果剩余时间小于等于0,或者超过了延迟时间(可能是由于系统时间调整),则执行函数
    if (remaining <= 0 || remaining > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      lastCall = now;
      func.apply(context, args);
    } else if (!timer && trailing) {
      // 如果没有定时器,并且需要在事件结束后执行,则设置定时器
      timer = setTimeout(() => {
        lastCall = leading ? Date.now() : 0;
        timer = null;
        func.apply(context, args);
      }, remaining);
    }
  };
  
  // 添加取消方法
  throttled.cancel = function() {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    lastCall = 0;
  };
  
  return throttled;
}

节流函数的使用

基本使用

javascript
// 定义一个滚动处理函数
function handleScroll() {
  console.log('Scroll position:', window.scrollY);
  // 执行滚动相关的操作
  // 例如,懒加载图片、更新导航栏样式等
}

// 创建一个节流函数
const throttledScroll = throttle(handleScroll, 100);

// 绑定到滚动事件
document.addEventListener('scroll', throttledScroll);

使用选项参数

javascript
// 定义一个点击处理函数
function handleClick() {
  console.log('Button clicked');
  // 发送网络请求
  // fetch('https://api.example.com/data')
  //   .then(response => response.json())
  //   .then(data => console.log(data));
}

// 创建一个节流函数,设置为不立即执行,只在事件结束后执行
const throttledClick = throttle(handleClick, 300, { leading: false, trailing: true });

// 绑定到按钮点击事件
const button = document.getElementById('button');
button.addEventListener('click', throttledClick);

使用取消功能

javascript
// 定义一个鼠标移动处理函数
function handleMouseMove(event) {
  console.log('Mouse position:', event.clientX, event.clientY);
  // 执行鼠标移动相关的操作
  // 例如,跟随鼠标移动的元素、绘制轨迹等
}

// 创建一个带取消功能的节流函数
const throttledMouseMove = throttle(handleMouseMove, 100);

// 绑定到鼠标移动事件
document.addEventListener('mousemove', throttledMouseMove);

// 在需要时取消节流
const cancelButton = document.getElementById('cancel');
cancelButton.addEventListener('click', function() {
  throttledMouseMove.cancel();
  console.log('Throttle cancelled');
});

节流函数的注意事项

1. this指向问题

在节流函数中,需要保存原始函数的this指向,否则在调用原始函数时,this会指向undefined(在严格模式下)或全局对象(在非严格模式下)。

javascript
function throttle(func, delay) {
  let lastCall = 0;
  return function() {
    const context = this; // 保存this指向
    const args = arguments;
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(context, args); // 使用apply绑定this指向
    }
  };
}

2. 参数传递问题

在节流函数中,需要保存原始函数的参数,否则在调用原始函数时,无法传递参数。

javascript
function throttle(func, delay) {
  let lastCall = 0;
  return function() {
    const context = this;
    const args = arguments; // 保存参数
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func.apply(context, args); // 传递参数
    }
  };
}

3. 内存泄漏问题

在使用节流函数时,需要注意内存泄漏问题。如果节流函数被绑定到DOM元素的事件上,当DOM元素被移除时,需要取消节流函数,否则节流函数会一直存在于内存中。

javascript
// 绑定节流函数
const throttledScroll = throttle(handleScroll, 100);
document.addEventListener('scroll', throttledScroll);

// 当DOM元素被移除时,取消节流函数
document.removeEventListener('scroll', throttledScroll);
throttledScroll.cancel();

节流函数与防抖函数的区别

节流函数和防抖函数都是用于减少函数调用次数的技术,但它们的实现原理和应用场景有所不同:

特性节流函数防抖函数
核心思想在事件触发的过程中,每隔一段时间执行一次函数调用在事件触发后的一段时间内,只执行最后一次函数调用
执行时机事件触发后立即执行,然后在一段时间内不再执行事件触发后等待一段时间再执行,若期间有新事件则重新计时
应用场景滚动事件、鼠标移动事件、游戏中的动画效果等搜索框输入、表单验证、窗口大小调整等

面试常见问题

1. 什么是节流?它的应用场景是什么?

  • 节流的定义:节流是一种性能优化技术,在事件触发的过程中,每隔一段时间执行一次函数调用,无论期间触发了多少次事件。
  • 应用场景:滚动事件、鼠标移动事件、游戏中的动画效果、网络请求等。

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

节流函数的实现有两种方式:

  • 时间戳实现:记录上次执行函数的时间戳,当事件触发时,计算当前时间戳与上次执行时间戳的差值,如果差值大于等于延迟时间,则执行函数,并更新上次执行时间戳。
  • 定时器实现:在事件触发时,启动一个定时器,如果在定时器到期前又触发了事件,则忽略该事件。当定时器到期时,执行函数,并清除定时器。

3. 节流函数中的this指向问题如何解决?

在节流函数中,需要保存原始函数的this指向,然后在调用原始函数时使用applycall绑定this指向。

4. 节流函数与防抖函数的区别是什么?

  • 核心思想:节流函数在事件触发的过程中每隔一段时间执行一次函数调用;防抖函数在事件触发后的一段时间内只执行最后一次函数调用。
  • 执行时机:节流函数在事件触发后立即执行,然后在一段时间内不再执行;防抖函数在事件触发后等待一段时间再执行,若期间有新事件则重新计时。
  • 应用场景:节流函数适用于滚动事件、鼠标移动事件等场景;防抖函数适用于搜索框输入、表单验证等场景。

5. 如何为节流函数添加选项参数?

可以添加选项参数,如leading(是否在事件触发后立即执行)和trailing(是否在事件结束后执行),以提高函数的灵活性。

6. 如何为节流函数添加取消功能?

可以在返回的函数上添加一个cancel方法,用于清除定时器和重置上次执行时间戳,取消节流函数的执行。

总结

节流函数是一种常用的性能优化技术,它可以减少函数的调用次数,提高代码的执行效率。节流函数的核心思想是:在事件触发的过程中,每隔一段时间执行一次函数调用,无论期间触发了多少次事件。节流函数在滚动事件、鼠标移动事件、游戏中的动画效果、网络请求等场景中特别有用。

实现节流函数时,需要注意以下几点:

  1. 保存原始函数的this指向,避免this指向错误
  2. 保存原始函数的参数,避免参数丢失
  3. 处理内存泄漏问题,在不需要时取消节流函数
  4. 可以添加选项参数和取消功能,提高函数的灵活性

节流函数和防抖函数都是用于减少函数调用次数的技术,但它们的实现原理和应用场景有所不同,需要根据具体场景选择合适的技术。

好好学习,天天向上