0

0

JS 防抖与节流实现原理 - 控制高频事件回调的执行频率优化

夜晨

夜晨

发布时间:2025-09-22 16:08:01

|

1059人浏览过

|

来源于php中文网

原创

防抖是事件停止触发后延迟执行一次,适用于搜索输入、窗口resize等场景;节流是固定时间间隔内最多执行一次,适用于滚动加载、鼠标移动等高频持续触发场景。两者均通过定时器控制执行频率,解决高频事件导致的性能问题,核心在于合理选择应用场景并处理this指向、参数传递及执行时机等问题。

js 防抖与节流实现原理 - 控制高频事件回调的执行频率优化

JavaScript 中的防抖(Debounce)与节流(Throttle)是两种核心的性能优化策略,它们通过控制高频事件回调函数的执行频率,有效避免了因事件密集触发导致的浏览器性能下降、资源浪费甚至页面卡顿。简单来说,防抖是“你尽管触发,我只在你停止触发一段时间后执行一次”,而节流则是“你尽管触发,我保证在一段时间内最多只执行一次”。这两种机制就像是给事件处理函数加了“限速器”或““冷却时间”,确保了用户体验的流畅性。

解决方案

理解防抖和节流的实现原理,其实就是理解如何用定时器(

setTimeout
)来管理函数执行的时机。

防抖(Debounce)的实现

防抖的核心思想是:当事件被触发时,不立即执行回调函数,而是设置一个定时器。如果在定时器设定的时间间隔内,事件再次被触发,那么就清除上一个定时器,并重新设置一个新的定时器。只有当事件在指定时间间隔内没有再次被触发时,回调函数才会被执行。

我个人觉得防抖就像是你在等电梯,如果不断有人按楼层,电梯就一直开着门等你,直到没人按了,它才真正关门上行。这种等待机制在很多交互场景下简直是救星。

function debounce(func, delay) {
  let timeoutId; // 用于存储定时器ID

  return function(...args) {
    const context = this; // 保存函数执行时的this上下文

    clearTimeout(timeoutId); // 每次事件触发都清除上一个定时器

    timeoutId = setTimeout(() => {
      func.apply(context, args); // 在延迟后执行函数,并传递正确的this和参数
    }, delay);
  };
}

// 示例用法:
// const myEfficientFn = debounce(() => console.log('我被执行了!'), 500);
// window.addEventListener('resize', myEfficientFn); // 窗口大小调整停止500ms后才执行

节流(Throttle)的实现

节流的核心思想是:在指定的时间间隔内,无论事件被触发多少次,回调函数都只会被执行一次。它保证了函数执行的频率上限。

节流则更像是坐公交车,每隔固定时间发一班,不管这期间有多少人在等,到了点就走。它保证了执行频率的上限,不会让事件处理彻底停滞,又能避免资源过度消耗。

function throttle(func, delay) {
  let inThrottle = false; // 标记是否在节流期内
  let timeoutId;

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

    if (!inThrottle) {
      func.apply(context, args); // 立即执行一次
      inThrottle = true;

      timeoutId = setTimeout(() => {
        inThrottle = false; // 延迟结束后,重置标记,允许下次执行
        // 如果有最后一次触发的事件,且在节流期内,可以考虑在这里执行一次,实现“尾部触发”
        // 但基础的节流通常不包含这个,需要额外逻辑处理
      }, delay);
    }
  };
}

// 示例用法:
// const myEfficientScroll = throttle(() => console.log('滚动事件被节流了!'), 200);
// window.addEventListener('scroll', myEfficientScroll); // 滚动事件每200ms最多执行一次

上面这个节流实现是“立即执行”版本,即在节流期开始时立即执行一次。还有一种常见的“延迟执行”版本,即在节流期结束后才执行。实际应用中,通常会结合使用或者根据需求选择。

为什么高频事件处理会成为前端性能瓶颈?

我们经常会遇到用户疯狂滚动页面、快速输入搜索词或者拖拽元素的情况。如果不加限制,每次事件触发都执行回调,浏览器就得拼命工作,轻则卡顿,重则直接崩溃。这就像你给一个服务器发了成千上万个请求,它当然受不了。

高频事件之所以成为前端性能瓶颈,主要有以下几个原因:

  • DOM 操作的昂贵性: 每次对 DOM 进行读写操作,都可能触发浏览器的重排(reflow)和重绘(repaint)。重排会计算元素在文档中的位置和大小,重绘则是在屏幕上绘制像素。这些操作都是非常消耗性能的,尤其是在复杂的页面布局中。想象一下,如果一个
    mousemove
    事件每毫秒触发一次,并且每次都修改元素的样式,那浏览器就得不停地重排重绘,页面自然就卡死了。
  • 网络请求的滥用: 比如搜索框的
    input
    事件,用户每输入一个字符就发送一次请求去后端搜索,这不仅会给服务器带来巨大压力,也会因为频繁的网络请求而阻塞浏览器,导致用户体验极差。
  • JavaScript 执行阻塞: 浏览器是单线程的,JavaScript 的执行会占用主线程。如果高频事件的回调函数中包含复杂的计算逻辑,它会长时间占用主线程,导致页面无法响应用户的其他操作,比如点击、滚动,甚至动画都会变得卡顿。
  • 资源消耗: 不受控制的高频事件还会导致内存泄露,或者持续消耗 CPU 和 GPU 资源,最终让设备风扇狂转,电量告急。

所以,防抖和节流就像是给这些“急性子”的事件处理函数套上了缰绳,让它们在合理的频率下工作,而不是一味地“瞎跑”。

拍我AI
拍我AI

AI视频生成平台PixVerse的国内版本

下载

防抖与节流在实际项目中如何选择与应用?

选择防抖还是节流,其实没有绝对的对错,关键在于你的业务场景和用户体验预期。我通常会问自己:这个操作是需要用户“完成”后才触发,还是需要“持续”但有频率限制地触发?

防抖的典型应用场景:

  • 搜索框输入(
    input
    事件):
    用户在搜索框输入文字时,我们通常不希望每输入一个字符就立即发送一次搜索请求。而是希望用户停止输入一段时间后(比如500毫秒),才发送请求。这样可以减少不必要的网络请求,提升用户体验。
  • 窗口调整(
    resize
    事件):
    当用户调整浏览器窗口大小时,页面布局可能需要重新计算。如果每次像素变化都触发重新布局,会非常卡顿。防抖可以确保只有在用户停止调整窗口后,才执行一次布局计算。
  • 表单验证: 用户在填写表单时,可能希望在输入完毕后才进行验证,而不是每输入一个字符就立即提示错误。
  • 按钮点击: 防止用户在短时间内重复点击按钮,导致多次提交表单或触发多次操作(例如,防止重复创建订单)。

节流的典型应用场景:

  • 页面滚动(
    scroll
    事件):
    滚动加载更多内容(懒加载)、滚动动画、滚动进度条等场景。我们希望在用户滚动时持续触发某些操作,但又不想频率过高。例如,每隔200毫秒检查一次是否需要加载新图片,而不是每次滚动都检查。
  • 鼠标移动(
    mousemove
    事件):
    拖拽功能、绘制图形、游戏中的角色移动等。这类操作需要持续反馈,但过高的频率会消耗大量资源。节流可以保证在一定时间内,鼠标位置更新的频率是可控的。
  • 游戏更新: 游戏循环中,某些物理计算或渲染操作需要持续进行,但如果帧率过高导致性能问题,可以通过节流来限制更新频率。

简单来说,如果你关心的是“结果”,即操作最终完成时的状态,那防抖更合适;如果你关心的是“过程”,即操作过程中需要持续反馈,但又想控制其频率,那节流更合适。

实现防抖与节流时有哪些常见的“坑”和优化技巧?

我自己刚开始写防抖节流的时候,最头疼的就是

this
指向问题,还有就是参数丢失。这些小细节,如果没处理好,调试起来真的让人抓狂。所以,我学到了,一个健壮的防抖/节流函数,除了核心逻辑,还得考虑这些边缘情况。

  1. this
    上下文问题:

    • 在事件监听器中,回调函数内部的
      this
      通常指向触发事件的 DOM 元素。但如果将回调函数包装在防抖/节流函数中,
      this
      的指向可能会丢失。
    • 解决方案: 使用
      func.apply(context, args)
      func.call(context, ...args)
      来确保原始函数的
      this
      上下文和参数被正确传递。ES6 的箭头函数也能很好地解决
      this
      绑定问题,因为它没有自己的
      this
      ,会捕获其所在上下文的
      this
    // 在上面的实现中,`const context = this;` 已经处理了这个问题。
    // 如果使用箭头函数,可以这样写:
    // return (...args) => {
    //   const context = this; // 这里的this是debounce/throttle的调用者,不是事件源
    //   clearTimeout(timeoutId);
    //   timeoutId = setTimeout(() => {
    //     func.apply(context, args);
    //   }, delay);
    // };
    // 更常见的做法是让闭包内的this指向包装函数被调用时的this,如示例代码所示。
  2. 参数传递问题:

    • 事件回调函数通常会接收一个事件对象
      event
      作为参数。防抖/节流函数需要确保这个参数也能正确地传递给被包装的原始函数。
    • 解决方案: 使用
      ...args
      收集所有传入的参数,并用
      apply
      call
      传递给原始函数。
    // 示例代码中的 `function(...args)` 和 `func.apply(context, args)` 已经处理了这个问题。
  3. 立即执行(

    leading
    edge)与延迟执行(
    trailing
    edge):

    • 防抖: 默认的防抖是“延迟执行”,即在停止触发后才执行。但有时我们希望在事件第一次触发时就立即执行一次,然后等待防抖期,如果期间再次触发则不再执行,直到防抖期结束后再允许下一次立即执行。这称为“立即执行”或
      leading
      edge。
    • 节流: 默认的节流也是“立即执行”一次,然后等待节流期。但也可以实现成“延迟执行”,即在节流期结束后才执行。
    • 优化技巧: 许多库(如 Lodash)提供了
      leading
      trailing
      选项来控制这些行为。自己实现时,需要额外逻辑来判断。
    // 带有立即执行选项的防抖
    function debounceWithLeading(func, delay, immediate = false) {
      let timeoutId;
      let invoked = false; // 标记是否在当前防抖周期内已经执行过
    
      return function(...args) {
        const context = this;
        const callNow = immediate && !invoked;
    
        clearTimeout(timeoutId);
    
        timeoutId = setTimeout(() => {
          invoked = false; // 定时器结束后重置标记
          if (!immediate) { // 如果不是立即执行模式,就在这里执行
            func.apply(context, args);
          }
        }, delay);
    
        if (callNow) { // 如果是立即执行模式且当前周期未执行过
          func.apply(context, args);
          invoked = true;
        }
      };
    }
  4. 取消(

    cancel
    )功能:

    • 有时我们希望能够手动取消一个正在等待执行的防抖或节流函数。
    • 优化技巧: 在返回的函数上添加一个
      cancel
      方法,用于清除定时器。
    // 带有取消功能的防抖
    function debounceWithCancel(func, delay) {
      let timeoutId = null;
      let debounced = function(...args) {
        const context = this;
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          func.apply(context, args);
        }, delay);
      };
    
      debounced.cancel = function() {
        clearTimeout(timeoutId);
        timeoutId = null;
      };
      return debounced;
    }
  5. 第三方库:

    • 对于生产环境,我个人强烈建议直接使用成熟的第三方库,例如 Lodash 的
      _.debounce
      _.throttle
      。这些库经过了大量测试,考虑了各种边缘情况,包括
      this
      绑定、参数传递、立即执行/延迟执行选项、取消功能等,实现得非常健壮和全面。自己手写虽然有助于理解原理,但在实际项目中往往不如直接使用库来得高效和稳定。
    // 使用 Lodash
    // import _ from 'lodash';
    // const myEfficientFn = _.debounce(() => console.log('我被执行了!'), 500, { leading: true });
    // const myEfficientScroll = _.throttle(() => console.log('滚动事件被节流了!'), 200, { trailing: false });

理解这些“坑”和优化技巧,能让你在实际开发中写出更健壮、更符合业务需求的防抖和节流函数。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

754

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

454

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1031

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

553

2023.09.20

AO3中文版入口地址大全
AO3中文版入口地址大全

本专题整合了AO3中文版入口地址大全,阅读专题下面的的文章了解更多详细内容。

1

2026.01.21

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.9万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.3万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号