阅读视图

发现新文章,点击刷新页面。

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

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

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

一、防抖

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

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

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

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

        <input type="text" id="test">
        <script>
            document.getElementById('test').addEventListener('keyup',function(){
                console.log(this.value);
            })
        </script>
    

    未防抖.gif

  • 防抖实现

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

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

    1. 延迟执行:每次调用 debouncedDoWhat 时,它会检查是否已有待执行的定时器(timer)。如果有,则清除之前的定时器,重新开始计时。
    2. 稳定后执行:只有在用户停止输入 3 秒(3000ms) 后,才会真正调用 doWhat 函数,并打印输入框的最新值。
        <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>
    

    防抖后的效果:

    防抖.gif

    关于上述代码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。

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

二、节流

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

  • 防抖实现

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

        <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,可以修改定时器的定时时间来解决

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

    运行效果

    节流.gif

告别刷新就丢数据!localStorage 全面指南

localStorage:网页本地存储

在现代 Web 开发中,为了提升用户体验和减少服务器请求,前端常常需要将一些数据缓存在用户的浏览器中。HTML5 提供了多种客户端存储方案,其中 localStorage 是最常用的一种持久化存储机制。

一、什么是 localStorage

localStorage 是一种由浏览器提供的 Web Storage API,它允许网页在用户的浏览器中长期保存键值对数据。与 sessionStorage 不同的是,localStorage 的数据不会因为浏览器关闭或页面刷新而丢失,只有当用户主动清除浏览器缓存或者通过代码显式删除时,这些数据才会被清除。

二、基本特性

  • 持久性:数据可以在浏览器中长期保存

  • 作用域:同源策略限制(相同协议 + 域名 + 端口)

  • 容量限制:通常最大为 5MB(视浏览器而定),若数据比5MB还大可以使用indexDB

  • 数据类型:只能存储字符串,复杂类型需序列化

        const person = {
            name: "张三",
            age: 18,
        }
        localStorage.setItem('personName', person); // 没有序列化
        localStorage.setItem('person', JSON.stringify(person)); // 序列化
    
    let persons = JSON.parse(localStorage.getItem('persons')); // 将json字符串zh
    

    image-20250710113917773.png

  • 同步操作:所有操作都是同步的,可能影响性能

三、常用方法

localStorage 提供了一组简单易用的方法来进行数据操作:


localStorage.setItem('key', 'value');// 存储数据

const value = localStorage.getItem('key');// 获取数据

localStorage.removeItem('key');// 删除数据

localStorage.clear();// 清空所有数据

const keyName = localStorage.key(n);// 获取第 n 个 key 的名称

const length = localStorage.length;// 获取当前存储项的数量

以一个增加列表项为例

输入姓名年龄后存储到localStorage,再次刷新页面数据依然存在。

html代码

    <form action="">
      姓名<input
        type="text"
        name="username"
        id="username"
        placeholder="请输入姓名"
        required
      /><br />
      年龄<input
        type="text"
        name="userage"
        id="userage"
        placeholder="请输入年龄"
        required
      />
      <input type="submit" value="提交" />
    </form>
    <ul id="person-list"></ul>

js代码

        
      const key = "persons";
// 添加数据
      document.querySelector("form").addEventListener("submit", (e) => {
        e.preventDefault();
        const username = e.target.username.value;
        const userage = e.target.userage.value;
        if (!username || !userage) return;
        const person = {
          username: username,
          userage: userage,
        };
        let persons = JSON.parse(localStorage.getItem(key)) || [];
        persons.push(person);
        localStorage.setItem(key, JSON.stringify(persons));
        refush();
      });

      document.addEventListener("DOMContentLoaded", () => {
        refush();
      });

 // 读取localStorage中的数据并展示
      function refush() {
        const personUl = document.getElementById("person-list");
        const persons = JSON.parse(localStorage.getItem(key)) || [];
        if(persons.length === 0){
          personUl.innerHTML = "暂无数据";
          return;
        }
        personUl.innerHTML = "";
        persons.forEach((person) => {
          const li = document.createElement("li");
          li.innerHTML = `${person.username} ${person.userage}`;
          personUl.appendChild(li);
        });
      }
    

初始时没有任何数据

image-20250710114741636.png localStorage存储实现,刷新后数据不丢失

ovwhe-dqle5.gif

四、用途与注意事项

localStorage适合存储用户的非敏感偏好设置(如主题、语言),缓存静态数据(如菜单结构、地区列表),实现轻量级的状态管理(如登录态标记)

不适合存储敏感信息(如密码、token),容易受到 XSS 攻击。存储大量结构化数据时,应考虑使用 IndexedDB,也不是和存储频繁更新的数据,避免阻塞主线程

六、与其他存储方式对比

存储方式 生命周期 容量 类型 安全性 适用场景
localStorage 永久 5MB 左右 字符串 低(易受 XSS) 轻量缓存、偏好设置
sessionStorage 浏览器标签关闭即失效 5MB 左右 字符串 单次会话数据
Cookie 可设置过期时间 4KB 左右 字符串 中(可加密传输) 登录态、跟踪用户
IndexedDB 永久 几百 MB 到 GB 级 结构化数据 大数据、离线应用
Web Worker Cache 自定义 无明确限制 可缓存资源 PWA、Service Worker 缓存
❌