【JavaScript手写篇】函数式编程之手写节流函数

前言

在前端开发中,我们经常需要处理一些高频触发但只需定期响应的事件,比如:

  • 用户滚动页面(scroll
  • 鼠标移动(mousemove
  • 窗口缩放(resize
  • 游戏中的按键监听

如果不加以控制,这些事件可能在 1 秒内触发上百次,导致:

  • 页面重绘/重排频繁
  • 动画卡顿、性能下降
  • 接口请求过多,服务器压力大

这时,节流(Throttle) 就派上了用场。

本文将带你从 定义、原理、实现过程、使用场景到注意事项,全面掌握 JavaScript 节流机制,与上一篇《防抖》形成完整对比,助你彻底搞懂函数限频优化。


一、什么是节流?(Definition)

定义

节流(Throttle) 是一种函数优化技术,它的核心思想是:无论事件触发多频繁,函数在指定时间周期内最多只执行一次

换句话说:

我不可能每次都响应你,但我保证每隔一段时间就执行一次。

类比理解

想象你坐地铁,每 5 分钟发一班车。
即使你在 5 分钟内多次“请求”地铁,它也只会在 固定时间点 发车。

  • 地铁发车 = 执行函数
  • 乘客上车 = 事件触发
  • 5 分钟间隔 = 节流的等待时间(wait

节流就是:固定频率执行,不因触发频繁而加速


二、节流的原理(Principle)

节流的实现依赖于 JavaScript 的两个核心机制:

  1. 闭包(Closure):保存上一次执行的时间戳或定时器状态
  2. 时间判断或定时器:控制函数是否可以执行

执行流程图(时间戳版本)

事件触发
   ↓
获取当前时间 now
   ↓
计算距离上次执行的时间差 diff = now - previous
   ↓
如果 diff >= wait,则执行函数,并更新 previous = now
   ↓
否则,不执行,等待下一次触发

执行流程图(定时器版本)

事件触发
   ↓
如果当前没有定时器(timer 为 null)
   ↓
设置定时器,wait 毫秒后执行函数,并清空 timer
   ↓
如果已有定时器,则不设置,等待其执行

三、手写节流函数(Implementation)

基础版本(时间戳实现)

/**
 * 节流函数(时间戳版本):在 wait 时间内最多执行一次
 * @param {Function} func - 要节流的函数
 * @param {number} wait - 等待时间(毫秒)
 * @returns {Function} - 包装后的节流函数
 */
function throttle(func, wait) {
  let previous = 0; // 上一次执行的时间戳

  return function (...args) {
    const context = this;
    const now = Date.now();

    if (now - previous >= wait) {
      func.apply(context, args);
      previous = now;
    }
  };
}

进阶版本(定时器实现)

/**
 * 节流函数(定时器版本):保证最后一次触发也能执行
 * @param {Function} func - 要节流的函数
 * @param {number} wait - 等待时间(毫秒)
 * @returns {Function} - 包装后的节流函数
 */
function throttle(func, wait) {
  let timeout = null;

  return function (...args) {
    const context = this;

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

完整版本(双剑合璧:时间戳 + 定时器)

/**
 * 节流函数(最终版):首次立即执行,末次也保证执行
 * @param {Function} func - 要节流的函数
 * @param {number} wait - 等待时间(毫秒)
 * @returns {Function} - 包装后的节流函数
 */
function throttle(func, wait) {
  let previous = 0;
  let timeout = null;

  const later = function (...args) {
    previous = Date.now();
    timeout = null;
    func.apply(this, args);
  };

  return function (...args) {
    const context = this;
    const now = Date.now();
    const remaining = wait - (now - previous);

    if (remaining <= 0 || remaining > wait) {
      // 如果可以执行,清除定时器,立即执行
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
    } else if (!timeout) {
      // 否则,设置定时器,保证末次执行
      timeout = setTimeout(later.bind(context, ...args), remaining);
    }
  };
}
参数说明:
参数类型说明
funcFunction要节流的函数
waitNumber等待时间(毫秒)

四、使用场景与示例

1. 页面滚动事件(监听滚动位置)

const handleScroll = throttle(() => {
  console.log('当前滚动位置:', window.scrollY);
  // 可用于:吸顶导航、懒加载、滚动动画
}, 100);

window.addEventListener('scroll', handleScroll);

效果:每 100ms 最多执行一次,避免频繁计算。


2. 鼠标移动事件(绘制轨迹)

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let points = [];

const drawPoint = throttle((x, y) => {
  points.push({ x, y });
  ctx.fillRect(x, y, 2, 2);
}, 16); // 约 60fps

canvas.addEventListener('mousemove', (e) => {
  drawPoint(e.clientX, e.clientY);
});

避免绘制过多点,影响性能。


3. 游戏按键监听

const shoot = throttle(() => {
  console.log('发射子弹!');
}, 500); // 每 500ms 最多发射一次

document.addEventListener('keydown', (e) => {
  if (e.key === ' ') {
    shoot();
  }
});

防止玩家“连点”过快,影响游戏平衡。


五、注意事项与最佳实践

1. 选择合适的实现方式

版本优点缺点适用场景
时间戳版首次立即执行停止触发后,最后一次可能不执行首次必须响应
定时器版保证末次执行首次有延迟末次必须响应
双剑合璧版首次立即 + 末次保证代码稍复杂推荐使用

推荐使用“双剑合璧”版本,兼顾用户体验。

2. this 指向问题

  • 使用 applycall 绑定原始 this
  • 箭头函数会改变 this,慎用

3. 与防抖(Debounce)的区别

特性节流(Throttle)防抖(Debounce)
执行时机固定频率执行最后一次触发后执行
适用场景滚动、鼠标移动、游戏搜索、提交、resize
触发频率至少执行一次可能一次都不执行

简单记:节流是“每隔一段时间”,防抖是“最后一次”

4. 不要滥用节流

  • 节流适用于“定期响应”的场景
  • 不适用于“必须立即响应”的场景(如按钮点击)

六、React/Vue 中的节流实践

Vue 3 + Composition API

import { onMounted, onUnmounted } from 'vue';

export default {
  setup() {
    const handleScroll = throttle(() => {
      console.log('滚动中...');
    }, 100);

    onMounted(() => {
      window.addEventListener('scroll', handleScroll);
    });

    onUnmounted(() => {
      window.removeEventListener('scroll', handleScroll);
    });

    return {};
  }
}

React + useCallback

import { useCallback, useEffect } from 'react';

function ScrollComponent() {
  const throttledScroll = useCallback(
    throttle(() => {
      console.log('滚动位置:', window.scrollY);
    }, 100),
    []
  );

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

  return <div style={{ height: '200vh' }}>滚动我</div>;
}

总结

要点说明
核心思想固定频率执行,最多一次
实现机制时间戳判断 或 定时器控制
关键技巧保存时间戳、处理末次执行、绑定 this
适用场景滚动、鼠标移动、游戏、动画
注意事项区分防抖、避免滥用、选择合适版本
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值