从点击到执行:如何优雅地控制高频事件触发频率

从点击到执行:如何优雅地控制高频事件触发频率

在网页开发中,某些事件(如窗口调整大小、滚动、键盘输入或鼠标移动)可能会频繁的被触发。如果每次事件触发都执行相应的处理函数,尤其是这些函数涉及复杂的计算或网络请求时,会导致性能问题,甚至可能使页面变得缓慢或无响应。为了解决这个问题,通常会采用两种技术:防抖(Debouncing)节流(Throttling)

一、防抖

防抖的目的是确保某个函数在短时间内不会被频繁调用。只有在停止触发一段时间后才执行一次。就是说事件停止促发后并过了设置的时间。适用于搜索框的输入、窗口大小调整、表单提交按钮防止双击。

实现原理:当一个事件发生时,设置一个定时器,在指定的时间间隔后执行特定的函数。如果在这个时间间隔内该事件再次被触发,则重置定时器。这意味着只有当事件停止触发超过设定的时间后,函数才会被执行。

  • 以键盘输入为例看看不做防抖是什么情况

    每次按键都会立即触发 console.log(this.value);,如果用户快速输入(如输入 "hello"),console.log 会被调用 5 次,它进行了多次无意义的执行。

    html 复制代码
        <input type="text" id="test">
        <script>
            document.getElementById('test').addEventListener('keyup',function(){
                console.log(this.value);
            })
        </script>
  • 防抖实现

    当用户在输入框(<input id="test">)中键入内容时,每次按键释放(keyup 事件)都会触发事件监听器。该监听器获取输入框的当前值(e.target.value)并传递给 debouncedDoWhat 函数。

    debouncedDoWhat 是一个经过防抖处理的函数,由 debounce(doWhat, 3000) 生成。防抖的核心逻辑是:

    1. 延迟执行 :每次调用 debouncedDoWhat 时,它会检查是否已有待执行的定时器(timer)。如果有,则清除之前的定时器,重新开始计时。
    2. 稳定后执行 :只有在用户停止输入 3 秒(3000ms) 后,才会真正调用 doWhat 函数,并打印输入框的最新值。
    html 复制代码
        <input type="text" id="test" />
        <script>
          document.getElementById("test").addEventListener("keyup", (e) => {
            debouncedDoWhat(e.target.value);
          });
    
          const debouncedDoWhat = debounce(doWhat, 3000);
    
          function doWhat(value) {
            console.log(value);
          }
    
          function debounce(fn, delay) {
            let timer = null;
            return function (...args) {
              const context = this;
              if (timer) {
                clearTimeout(timer);
              }
              timer = setTimeout( ()=> {
                fn.apply(context, args);
              }, delay);
            };
          }
        </script>

    防抖后的效果:

    关于上述代码this指向问题

    const debouncedDoWhat = debounce(doWhat, 3000);可知debouncedDoWhat实际是就是debounce返回的闭包函数。闭包函数中执行const context = this;捕获的是debouncedDoWhat调用时的this,而debouncedDoWhat是作为普通函数被调的,所以此时捕获的this指向window。在定时器中的函数是箭头函数,它的this指向的是返回函数也就是debouncedDoWhat,所以这里可以不使用context来保存this。最后执行fn.apply(context, args);将fn的this指向debouncedDoWhat指向的this。

    js 复制代码
          function debounce(fn, delay) {
            let timer = null;
            return function (...args) {
              if (timer) {
                clearTimeout(timer);
              }
              timer = setTimeout(()=> {
                fn.apply(this, args);
              }, delay);
            };
          }

二、节流

节流的目的是使函数在一段时间内触发一次,也就是说在规定的时间间隔内最多只执行一次事件处理函数,即使在这段时间内事件被多次触发。适用于滚动加载、鼠标移动等事件。

  • 防抖实现

    代码实现的功能是在一段时间内( 5秒)只执行一次函数,即使事件被频繁触发。

    html 复制代码
        <div>
          <input type="text" id="inputC" />
        </div>
        <script>
          function throttle(fn, delay) {
            let last; // 存储上一次函数成功执行的时间戳
    
            let deferTimer; // 存储 setTimeout 返回的 ID,用于清除定时器
    
            return function (...args) {
              let that = this; // 保存 this 上下文,确保在 setTimeout 中也能正确访问 this
    
              let now = +new Date(); // 获取当前时间戳(+new Date() 是一种快速获取时间戳的方式)
    
              // 判断是否在节流周期内
              if (last && now < last + delay) {
                clearTimeout(deferTimer); // 如果还在限制时间内,则清除之前的定时器,并重新设置新的定时器
                deferTimer = setTimeout(function () {
                  // 当定时器触发时,更新 last 时间为当前时间
                  last = now;
    
                  // 执行原始函数,并传递参数
                  fn.apply(that, [...args]);
                }, delay); // 延迟执行到下一个周期开始
              } else {
                // 如果是第一次触发或者不在节流周期内,直接执行函数
    
                // 更新 last 为当前时间
                last = now;
                fn.apply(that, [...args]);// 执行原始函数,并传递参数
              }
            };
          }
          
    
          // 使用示例
          document.getElementById("inputC").addEventListener(
            "keyup",
            throttle(function (e) {
              console.log(e.target.value);
            }, 5000)
          );
        </script>

    代码的执行逻辑

    1. 调用throttle函数后还会返回一个闭包函数,并形成一个闭包,其中的自由变量last记录的是上一层原始函数 执行的时间。deferTimer记录的是定时器id,用于清除定时器

    2. 执行返回的闭包函数,首先保存当前上下文that并获取当前时间戳now

    3. 判断是否在节流周期内,若是第一次触发(last为undefined时)则直接执行原始函数。若不是第一次触发事件则进入if中

    4. 在if中首先清除上一次的定时器并设置新的定时器,在延迟时间(delay)后执行:

      在定时器中更新last为当前时间并使用apply调用原始函数,确保正确的this和参数传递

    总的来说就是

    • 若是事件是第一次触发则直接走else线执行一次,并设置上一次的执行时间点。

    • 若事件一直触发那么代码走的一直都是if线 ,这个定时器也一直处于刷新的状态而不会执行里面的原始函数 。所以last一直都是上一次执行时的last,随着时间的推进会有last + delay < now,也就是说距离上一次函数执行已经过去了delay,这时候就会走else线执行原始函数

    • 若事件一直触发了几次之后停止触发 (此时last + delay < now),由于存在一个定时器,这个定时器会在触发停止的delay时间后执行原始函数

      这里最后一次会延迟执行,也就是说这次执行到上一次执行的时间间隔大于delay,可以修改定时器的定时时间来解决

      js 复制代码
                  deferTimer = setTimeout(function () {
                    // 当定时器触发时,更新 last 时间为当前时间
                    last = now;
      
                    // 执行原始函数,并传递参数
                    fn.apply(that, [...args]);
                  }, delay+delay-now); // 距离下一次执行时间越近越小,直到为0

    运行效果

相关推荐
在钱塘江12 分钟前
《你不知道的JavaScript-上卷》-笔记-5-作用域闭包
前端
搬砖码13 分钟前
Vue病历写回功能:实现多输入框内容插入与焦点管理🚀
前端
不简说17 分钟前
史诗级更新!sv-print虽然不是很强,但却是很能打的设计器组件
前端·产品
用户952511514015518 分钟前
最常用的JS加解密场景MD5
前端
Hilaku19 分钟前
“虚拟DOM”到底是什么?我们用300行代码来实现一个
前端·javascript·vue.js
打好高远球25 分钟前
mo契官网建设与SEO实践
前端
蒟蒻小袁26 分钟前
力扣面试150题--全排列
算法·leetcode·面试
神仙别闹31 分钟前
基于Java+MySQL实现(Web)可扩展的程序在线评测系统
java·前端·mysql
心.c1 小时前
react当中的this指向
前端·javascript·react.js
Java水解1 小时前
Web API基础
前端