玩转抖动-防抖和节流

41 阅读14分钟

1.引言

1.1 何为抖动:

  • 首先我想说一下什么叫做抖动,抖动现象在日常的开发是很常见的一个现象。抖动是由于高频事件触发而产生的性能问题
  • 这种问题通常发生在用户交互密集的场景下,比如窗口的size变化,频繁滚动网页或在输入框快速打字等操作。
  • 如果不加以限制的话,会导致系统资源的过度消耗,从而严重影响应用的响应速度和用户的使用体验。

2.频繁事件处理的问题

2.1 性能瓶颈:

  • 当高频的事件频发触发时,它们会要求浏览器进行大量的计算和重绘,这将显著增加DOM的操作次数,最终导致页面卡顿,甚至崩溃。
  • 例如,当用户在窗口中调整大小或滚动页面时,浏览器需要重新计算布局和渲染页面,这会消耗大量的CPU资源。
  • 如果这些事件处理函数非常复杂,或者处理的数据量非常大,那么性能问题会更加严重。

2.2 用户体验影响:

  • 频繁的DOM操作和网络请求不仅消耗了系统的资源,还会导致用户界面的卡顿和反应迟钝。
  • 例如,用户在滚动长页面的时候,如果浏览器忙于处理每一次滚动事件导致的DOM更新的话,就会给用户一种“卡住”的感觉。
  • 同样地,如果一个输入框在修改时频繁发送网络请求,那么用户的输入会显得非常迟钝,这也会影响用户的体验,降低用户的满意度。

3.防抖(Debounce)技术

3.1 概念阐释:

  • 所以为了解决抖动这个性能问题,我们就需要去做一些优化策略。那么防抖就出现了。
  • 防抖其实本质上就是一种函数调用优化策略,就是当一个事件被频繁触发的时候,只有在最后一次事件发生后的指定时间内
  • 没有新的事件触发时,才会执行相应的函数。
  • 这就表示即使用户频繁触发同一事件,该函数也只会执行一次,从而避免了函数的频繁调用,导致的抖动现象。

3.2 应用场景:

防抖技术适用于那些短时间内连续触发相同事件的场景,常见的应用场景包括:

  • 搜索框输入实时搜索:当用户每一次更新搜索框内容时,每敲击一个字符都会触发输入事件,如果立刻发送网络请求获取搜索结果,这样会导致大量的无谓的网络请求。

    所以可以使用防抖技术当用户停止输入指定时间后,再发送网络请求,这样可以大大减少网络请求的次数,提高性能。

  • 窗口大小调整:当用户调整浏览器窗口大小时,浏览器会频繁触发resize事件,如果每次resize事件都执行一些复杂的计算,那么就会导致浏览器卡顿。

    所以可以使用防抖技术,当用户停止调整窗口大小时,再执行resize事件的处理函数,这样可以避免频繁的DOM操作,提高性能。

3.3 实现方法:

function debounce(func, wait) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        timeout = setTimeout(() => func.apply(this, args), wait);
    };
}

上面是一个简易版的防抖函数

  • 这个防抖函数接受两个参数,第一个参数是要执行的函数func,第二个参数是等待时间wait
  • 在函数内部,我们定义了一个变量timeout,用来存储定时器的ID
  • 当防抖函数被调用时,我们首先清除之前的定时器(如果存在的话),然后设置一个新的定时器,在等待时间wait后执行函数func
  • 这样,无论防抖函数被调用多少次,只有在最后一次调用后的等待时间wait内没有新的调用时,函数func才会被执行。

3.4 进阶技巧:

  • 防抖函数还可以接受一个可选的第三个参数immediate,用来指定是否在事件触发时立即执行函数func

  • 如果immediatetrue,那么函数func会在事件触发时立即执行,然后等待wait时间后,如果不再有新的事件触发,那么函数func会再次执行。

  • 如果immediatefalse,那么函数func会在事件触发后的wait时间后执行,如果在这段时间内有新的事件触发,那么函数func会重新计时。

  • 下面是一个带有immediate参数的防抖函数的实现:

function debounce(func, wait, immediate) {
    let timeout;
    return function(...args) {
        clearTimeout(timeout);
        if (immediate && !timeout) {
            func.apply(this, args);
        }
        timeout = setTimeout(() => {
            if (!immediate) {
                func.apply(this, args);
            }
            timeout = null;
        }, wait);
    };
}
  • 在实际使用中,我们还可以根据需求对防抖函数进行一些定制,比如可以根据不同的场景动态调整等待时间, 例如基于事件类型或用户行为的复杂性,或者添加一些额外的参数等。
  • 总之,防抖技术是一种非常实用的函数调用优化策略,可以帮助我们解决抖动问题,提高应用的性能和用户体验。

4.节流(Throttle)技术

4.1 概念对比:

  • 节流和防抖都是用来优化函数调用的技术,但是它们的工作原理和用途是不同的。
  • 节流技术是一种限制函数执行频率的策略,它会在指定的时间间隔内只执行一次函数,无论在这段时间内函数被调用了多少次。
  • 这就表示即使用户频繁触发同一事件,该函数也只会以固定的频率执行,从而避免了函数的频繁调用,导致的抖动现象。

4.2 应用场景:

节流技术适用于那些需要限制函数执行频率的场景,常见的应用场景包括:

  • 滚动加载更多:当用户滚动页面时,浏览器会频繁触发scroll事件,如果每次scroll事件都执行一些复杂的计算,那么就会导致浏览器卡顿。所以可以使用节流技术,可以确保处理函数在一定时间内只执行一次,保持页面滚动的流畅性。
  • 游戏帧率控制:在游戏开发中,需要控制游戏帧率,以保证游戏的流畅性。可以使用节流技术,当游戏帧率超过指定值时,只执行一次游戏逻辑,这样可以避免游戏帧率过高,导致游戏卡顿。
  • 数据上传/下载:在网络请求中,频繁的数据上传或下载可能会导致网络拥塞。通过节流限制上传或下载的频率,可以优化网络带宽的使用。

4.3 实现代码:

  • 实现节流的方法有两种,一种是使用时间戳,另一种是使用定时器

1. 时间戳实现:

const lastTime = 0;
const throttle = function(func, wait){
  const now = new Date();
  if(now - lastTime >= wait){
    func();
    lastTime = now;
  }
}
  • 上面时间戳的实现方式是记录上一次执行时的时间,
  • 然后每次调用函数时,获取当前时间,如果当前时间与上一次执行时间的差值大于等于等待时间,就执行函数,并更新上一次执行时间。

2. 定时器实现:

let timeoutId;
const throttle = function(func, wait){
  if(!timeoutId){
    timeoutId = setTimeout(() => {
      func();
      timeoutId = null;
    }, wait);
  }
}
  • 上面定时器的实现方式是使用一个定时器,当第一次调用函数时,设置一个定时器,在等待时间后执行函数,并将定时器ID保存下来。

  • 如果在这段时间内再次调用函数,那么会清除之前的定时器,并重新设置一个新的定时器。

  • 这样,无论函数被调用了多少次,只有在等待时间后才会执行一次函数。

  • 但是使用setTimeout函数我们之前也说过会造成调用的延迟。

  • 在这里你会发现为啥感觉和防抖有点像,但是仔细分析来看两者是有区别的。

    • 防抖是每一次进入的时候直接清除setTimeoutId,接着执行setTimeout函数并且重新设置setTimeoutId。这里相当于重新计时。
    • 节流是每一次进入的时候都去判断setTimeoutId是否存在,如果存在则不执行,如果不存在则执行并且设置setTimeoutId。这里相当于隔开一段时间。

4.4 高级用法:

在节流技术中,领先触发leading)和尾随触发trailing)是两个重要的概念: 领先触发:函数在时间间隔的开始立即执行,即在事件首次触发时就执行函数,随后在设定的时间间隔内不再执行,直到下一个周期开始。 尾随触发:函数在时间间隔的结束时执行,即在事件连续触发时,函数不会立即执行,而是等待时间间隔结束后才执行一次。

5.防抖与节流的选择

5.1 决策指南:

选择防抖还是节流,主要取决于你的具体需求和应用场景。以下是几个决策准则:

防抖: 当你需要在用户完成某个动作(如停止输入、停止滚动)后的一段时间再执行操作时,应使用防抖。 特别适用于需要减少网络请求或计算开销的场景,比如实时搜索、窗口尺寸调整、或在用户输入完成后执行异步验证节流: 当你需要控制函数的执行频率,确保在一定时间间隔内函数只执行一次时,应使用节流。 特别适用于处理频繁触发的事件,如滚动事件、鼠标移动、或需要平滑动画效果的场合。

5.2 综合运用:

在某些复杂场景下,你可能需要同时使用防抖和节流。 例如,在实现一个复杂的输入控件时,你可能希望在用户输入时实时显示搜索建议(使用节流来控制频率), 并在用户停止输入后自动提交搜索(使用防抖来等待用户完全停止输入)。

6.实战案例

6.1 实战需求:

下面我们来玩一个具体的需求:

  1. 实现无限滚动功能,并且在用户接近页面底部时自动加载更多内容

  2. 实现输入框中实时显示搜索建议

6.2 实战分析:

  • 首先我们要确定这两个具体的需求要根据上面的决策准测,来确定两个需求来选择防抖还是节流还是两者组合

  • 在用户接近页面底部时自动加载更多内容,那么就表示我们只需要在一定时间间隔内去获取内容就可以,并不需要它疯狂滚动时一直频繁的调用接口导致卡顿问题,所以这里我们用节流的方法即可。

  • 输入框中输入内容接着调用接口,这个动作后的一段时间再去调用接口即可,并不需要用户在修改的时候就频繁的调用接口,所以这里我们只需要使用防抖即可

6.3 实战代码:

  • HTML代码:

    <body>
      <div id="search-container">
        <input type="text" id="search-input" placeholder="Search...">
        <ul id="suggestions"></ul>
      </div>
      <div id="content">
        <div>初始内容</div>
      </div>
    </body>
    
  • JAVASCRIPT代码:

      <!--引入lodash库-->
      <script src="https://cdn.jsdelivr.net/npm/lodash/lodash.min.js"></script>
      <script>
        // 1. 实现无限滚动功能,并且在用户接近页面底部时自动加载更多内容
        /**
         * 使用isNearBottom函数检测是否接近页面底部。
         * 使用_.throttle来限制loadMore函数的调用频率,避免在快速滚动时发送过多网络请求。
         * 每次加载更多内容时,向页面底部追加10个条目。
         */
    
        /**
         * 判断的方法:窗口的高度的高度加上滚动位置和页面总高度进行比较。
         * 如果滚动位置加上视口高度接近或等于页面总高度,则说明用户靠近了底部。
         */
        // 检查是否接近页面底部
        function isNearBottom() {
          const triggerHeight = 100; // 触发加载的距离阈值
          // 窗口高度
          const windowHeight = window.innerHeight;
          // 滚动位置
          const scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
          // 页面总高度
          const pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
    
          console.log('是否靠近底部?', pageHeight - windowHeight - scrollPosition <= triggerHeight);
          return pageHeight - windowHeight - scrollPosition <= triggerHeight;
        }
    
        /**
         * 我们可以利用lodash库, 这个库提供了_.debounce和 _.throttle两个函数即防抖和节流
         * _.debounce可以接受一个wait参数来设置延迟时间,以及一个immediate参数来控制是否在第一次调用时立即执行。
         * _.throttle同样接受wait参数,但确保函数在设定的时间间隔内最多执行一次。
         */
        // 加载更多内容-使用节流
        const loadMore = _.throttle(() => {
          console.log('加载更多内容');
          fetch('https://jsonplaceholder.typicode.com/photos') // 使用一个公开的API作为示例
            .then(response => response.json())
            .then(data => {
              data.slice(0, 10).forEach(item => {
                const newItem = document.createElement('div');
                newItem.textContent = item.title;
                document.getElementById('content').appendChild(newItem);
              });
            })
            .catch(error => console.error('错误:', error));
        }, 500);
    
        // 2. 实现输入框中实时显示搜索建议
        /**
         * 使用_.debounce来限制searchSuggestions函数的调用,避免在用户快速输入时发送过多网络请求。
         * 每次用户输入时,清空并填充搜索建议列表。
         */
        const searchSuggestions = _.debounce(query => {
          fetch(`https://jsonplaceholder.typicode.com/users?username_like=${query}`) // 假设API支持这样的查询
            .then(response => response.json())
            .then(suggestions => {
              const suggestionsList = document.getElementById('suggestions');
              suggestionsList.innerHTML = '';
              suggestions.forEach(user => {
                const option = document.createElement('li');
                option.textContent = user.username;
                suggestionsList.appendChild(option);
              });
            })
            .catch(error => console.error('错误:', error));
        }, 300);
    
        // 页面加载完毕后注册事件监听器
        window.addEventListener('load', () => {
          window.addEventListener('scroll', () => {
            if (isNearBottom()) {
              loadMore();
            }
          });
    
          const inputElement = document.getElementById('search-input');
          inputElement.addEventListener('input', event => {
            searchSuggestions(event.target.value);
          });
        });
      </script>
    
  • CSS代码:

      <style>
        body {
          height: 2000px;
        }
    
        #content {
          margin-top: 2500px;
        }
    
        #search-container {
          position: fixed;
          top: 10px;
          left: 10px;
          z-index: 1000;
        }
      </style>
    

8.总结与展望

8.1 总结:

  • 防抖和节流出现的背景就是抖动,抖动就是高频事件触发而产生的性能问题

  • 防抖和节流都是防止抖动的一种优化策略,但是它们两者又是从不同维度来解决这个抖动问题的。

  • 防抖是从调用的时间来限制的,每一次有新的调用时候都会清空之前的计时,重新开始计时,直到满足最后一次的调用之后的一段时间之后才会去调用。这样就算用户频繁去做一个action的时候,函数只会调用一次。

  • 节流是从调用频率的维度来解决抖动这个问题的。它的简易实现方法有时间戳法和setTimeout,这两个方法也有所不同。

    • 时间戳法就是设置一个常用变量去标记上一次调用的时间,然后根据现在调用的时间进行相减,再和设置的wait时间比较,超过wait的时间即可调用,最后灵魂一步将常用变量上一次调用的时间设置当前的时间即可。
    • setTimeout,最灵魂的东西就是setTimeoutId,当这个idnull的时候我们就去调用方法,接着设置id,这样id就不会为null了,就能控制它的调用了。
  • 防抖和节流开发者还可以使用lodash库,这个库里面有已经封装好的_.debounce(防抖)和_.throttle(节流)方法,这里面可以传入wait参数去控制调用的时间间隔。 当然,开发者也可以自己开发定制的函数去实现,作者也在高级方法下面提到过。

  • 防抖和节流本身这两者并没有冲突,虽然它们的应用环境有所不同,只需要根据判断准则即可满足开发者的需要,并且这两者也可以组合使用。 根据不同的场景,开发者权衡去使用。

8.2 主流框架防抖和节流的实现:

  • React:有专门的库如 react-throttlereact-debounce,以及高阶组件(HOCs)和自定义 hooksuseDebounceuseThrottle,开发者可以在服务或组件中使用这些关键字或者库来防止抖动。
  • Vue.jsVue 自身的 v-model 指令允许使用修饰符来实现节流效果。同时,有像 vue-debouncevue-throttle-debounce 这样的插件,以及自定义的 Vue 指令和 mixins 可以直接在模板和组件中使用。
  • AngularRxJS 提供了 debounceTimethrottleTime 等操作符,可以很容易地在 Angular 应用中集成防抖和节流。开发者可以在服务或组件中使用这些操作符来处理异步数据流。

这些主流框架防抖和节流的玩法,少侠们自己去玩玩吧!后面玩到具体的框架的时候,我也会出相关的博客。