前端节流(throttle)和防抖动(debounce)

节流(throttle)

节流指的都是某个函数在一定时间间隔内只执行第一次回调。举个常见的节流案例:我们把某个表单的提交按钮——button 设成每三秒内最多执行一次 click 响应;当你首次点击后,函数会无视之后三秒的所有响应;三秒结束后,button 又恢复正常 click 响应功能,以此类推。

有什么用呢?通常,这类提交 button 的@click响应会给后端发送 api 请求,频繁的点击意味着频繁的请求(流量)——会给后端带来很大的压力;此外,这些回调请求返回后,往往会在前端响应其他事件(如刷新页面),可能导致页面不停的加载,影响用户体验。所以我们要给这个 button 添加节流函数,防止一些无意义的点击响应。

节流实现

节流的实现就要用到 js 高阶函数了。基础版 throttle 实现很简单:利用闭包记录前一次执行的时间戳,并判断本次点击和前一次点击的时间间隔,超过设定域值(如 3 秒)才响应函数,反之不响应:

const throttle = (cb, wait = 3000) =>{
  let previous = 0;
  return (...args) => {
    const now = +new Date();
    if( now - previous > wait ){
      previous = now;
      cb.apply(this, args);
    }
  }
}

例如滚动加载,滚动到底部了,数据正在加载,用户重复触发滚动到底部,这时就需要节流,没加载完之前,不会触发第二次 

这里的cb就是被执行的回调函数,wait是设定的时间间隔。具体使用的时候,只要给常规监听的回调函数套一层throttle方法即可:

$button.addEventListener("click", throttle(cabllback));

还有一种常见的实现是做个定时器锁,只是会延后执行首次响应事件(如 3 秒后再执行回调);不过,同样可以确保特定时间间隔内只执行一次响应。

const throttle = (cb, wait=3000) => {
  let timeId;
  return (...args) => {
    if( timeId ) return;
    timeId = setTimeout(() => {
      cb.apply(this, args);
      timeId = undefined;
    }, wait)
  }
}

防抖动(debounce)

所谓的抖动就是浏览器频繁布局时,由于算力不足导致的页面颤动现象。防抖动就是利用类似于节流的手段——无视短时间内重复回调,避免浏览器发生抖动现象的技术。限流和防抖动在设计思想上一脉相承,只是限流是在某段时间内只执行首次回调,而防抖动通常是只执行末次回调。

比较常见的抖动场景是在自动索引的搜索设计上;当我们在搜索框内输入不同索引时,页面会频繁计算索引并渲染列表,以致产生抖动。但事实上在这类场景里,有价值的请求只会发生在用户停止输入后,通俗来说就是用户输入过程中的字符串不必当真。

Debounce 就是用来过滤输入过程中无意义的响应。实现上,只需要设置一个定时器(setTimeout),并在定计时器启动后(如 3 秒后)执行这个回调函数;若在定时器启动前又有相同回调到来,便取消之前的定时器(clearTimeout)——之前的回调便取消了;然后再创一个新的定时器回调,如此反复。
function debounce(cb, wait = 3000) {
  let timer = null

  return (...args) => {
      if (timer) clearTimeout(timer)

      timer = setTimeout(() => {
        timer = null;
        cb.apply(this, args)
      }, wait)
  }
}

很多库在实现 debounce 的时候还喜欢加第三个参数——immediate:一个 boolean 参数,表示是否执行首次响应(默认是最后一次)。这算是防抖动和节流结合使用的实现了:

function debounce(cb, wait = 3000, immediate = false) {
  let timeout;

  return function(...args) {
    let callNow = immediate && !timeout;

    if (timeout) clearTimeout(timeout);

    timeout = setTimeout(() => {
      timeout = null;

      if (!immediate) cb.apply(this, args);
    }, wait);

    if (callNow) cb.apply(this, args);
  };
}
实现上也不难,这里timeout表示 debounce 是否已被调用,它与immediate共同决定是否立即执行回调函数——callNow。若 immediate 被设成了 true 并且没有开启的计时器(!timeout),则能被callNow,便会立即执行 cb(不会在 setTimeout 里执行)。其他实现与普通 debounce 相同。

转自简书:https://www.jianshu.com/p/11b206794dca

总结

  • 节流: 控制高频事件执行次数
  • 防抖:用户触发事件过于频繁,只要最后一次事件的操作

函数防抖是某一段时间内只执行一次;而函数节流是间隔时间执行,不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。

防抖是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,都会清除当前的 timer 然后重新设置超时调用,即重新计时。这样一来,只有最后一次操作能被触发。

节流是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。

demo:传入一个函数,返回的也是这个函数,调用这个函数三次后就不在调用

function transformer(func) {
  let i = 3;
  return function (...args) {
    const context = this;
    if (i > 0) {
      i--;
      return func.apply(context, args);
    } else {
      return;
    }
  }
}

虚拟滚动

前端虚拟滚动(Virtual Scrolling)是一种优化长列表渲染性能的技术,它的原理是只渲染可视区域内的元素,从而减少渲染开销,提高性能。具体实现时,可以将列表分成多个虚拟的块,只渲染当前可视区域内的块,其他块则不渲染。随着用户滚动列表,不断加载新的块,同时移除不可见的块,从而实现滚动效果。

虚拟滚动的原理可以分为两个方面:

  1. 块的划分:将长列表划分成多个虚拟的块,每个块包含一定数量的元素。块的大小通常根据可视区域的大小和元素的大小来确定,可以动态计算或手动设置。在初始渲染时,只渲染可视区域内的块,其他块则不渲染。
  2. 块的加载和移除:随着用户滚动列表,不断加载新的块,同时移除不可见的块,从而实现滚动效果。当用户滚动到新的块时,需要动态生成DOM元素并插入到可视区域内,同时移除不可见的块,以减少渲染开销。在加载和移除块时,需要进行一些优化,例如缓存块、预加载块等,以提高性能和用户体验。

需要注意的是,虚拟滚动的实现需要根据具体情况进行选择和优化,以达到最佳的性能和用户体验。同时,虚拟滚动也可能会带来一些问题,例如滚动条长度不准确、元素高度不一致等,需要在实现时进行充分考虑和测试。