逐步理解js防抖 => 深入防抖与节流

1,470 阅读6分钟

逐步理解js防抖

先抛开"防抖","节流"这两个名词。

先看一个需求场景,在下图中点击加漫游速度,弹窗提示,2秒后消失。此时,如果疯狂点击加,那么速度提示弹窗将会疯狂的显示隐藏。但是我们想要的效果不是这样。

需求

  • 1.正常点击加减按钮,显示速度提示弹窗,2秒后隐藏弹窗
  • 2.连续点击加减按钮(间隔时间短),显示速度提示弹窗,操作停止后,2秒后隐藏弹窗

解决思路

  • 1.速度改变时,执行显示弹窗,定义setTimeOut定时器,2秒后执行隐藏弹窗
  • 2.正常情况,用户只点击一次,以上代码正常,如果用户点击频繁,代码1会多次执行, 这时需要clearTimeout清空定时器
  • 3.clearTimeout清空定时器,原本定时器在2秒后执行,这时清空就会取消原本定义的2秒后执行,只会执行最后一次的执行
  let speedChangeTimer;
  cameraWalkOperator.bindChangeSpeedCallback(() => {
    console.log("速度改变");
    this.setState({
      tipSpeed: true,
    });
    clearTimeout(speedChangeTimer);
    speedChangeTimer = setTimeout(() => {
      this.setState({
        tipSpeed: false,
      });
    }, 2000);

    let speed = cameraWalkOperator.getSpeed();
    this.setState({
      speedValue: speed,
    });
  });

防抖

对以上代码进行封装

// 这其实是个闭包函数
function debounce(func, wait) {
  let timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(function () {
      func();
    }, wait)
  }
}

闭包: 如果想让一个函数执行完后,函数内的某个变量(timer)仍旧保留,就可以使用闭。

把要保存的变量在父作用域声明,其他的语句放到子作用域里,并且作为一个function返回,所以闭包可以理解为分离变量

网上版本

function debounce(method,delay) {
  var timer=null;
  return function () {
    var context = this, args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      method.apply(context,args);
    },delay);
  }
}

想理解这里的this arguments apply; 请看我的这篇文章 juejin.cn/post/684490…

注意

  • 查看代码可知返回的是一个函数,没有执行
  • 使用时一般绑定在监听上document.addEventListener('mousemove', debounce)
  • 如果我们只是普通的使用,没有用dom的这种绑定事件监听,那么得加 () 自运行
  • 如下图所示,这个防抖其实是无效的,因为每次执行函数时,debounce都重新执行了一遍,已经不是同一个了,是不同的。

想理解addEventListener绑定事件原理。请看我的这篇文章

定义

定义: 策略是当事件被触发时,设定一个周期延迟执行动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。

深入防抖与节流

防抖(debounce)

防抖 — 指触发事件后,就是把触发非常频繁的事件合并成一次去执行。

即在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算。

生活中的例子

乘车刷卡的情景,只要乘客不断地在刷卡,司机师傅就不能开车,乘客刷卡完毕之后,司机会等待几分钟,确定乘客坐稳再开车。如果司机在最后等待的时间内又有新的乘客上车,那么司机等乘客刷卡完毕之后,还要再等待一会,等待所有乘客坐稳再开车。

debounce.png

代码实现

function debounce(fn, delay) {
  // 声明定时器
  let timer = null;

  // 利用闭包的形式来保存定时器
  return function () {
    let _this = this;
    let args = arguments
    clearTimeout(timer); // 再次调用防抖函数则先清空定时器
    timer = setTimeout(function () {
      fn.apply(_this, args); // 声明定时器(如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算)
    }, delay);
  }
}

function down () {
  console.log('down');
}

elementHtml.addEventListener('click',debounce(dj,1000))
// 1.在 click 事件上绑定处理函数,这时 debounce 函数会立即调用,实际上绑定的函数的 debounce 函数内部返回的函数。
// 2.每一次事件被触发,都会清除当前的 timer 然后重新设置超时调用。
// 3.只有在最后一次触发事件,才能在 delay 时间后执行。

节流(throttle)

节流 — 指频繁触发事件时,只会在指定的时间段内执行事件回调。

即触发事件间隔大于等于指定的时间才会执行回调函数。

区别:防抖动和节流的本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行

生活中的例子

生活中的水龙头,拧紧水龙头到某种程度会发现,每隔一段时间,就会有水滴流出。

throttle.png

代码实现

定时器版-闭包

function throttle (fn, delay) {
  // 声明定时器
  let timer = null;

  // 利用闭包的形式来保存定时器
  return function () {
    let _this = this;
    let args = arguments;

    if (!timer) { // 如果没有定时器则设置定时器(使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,所以当最后一次停止触发后,还会再执行一次函数。)
      timer = setTimeout(function () {
        fn.apply(_this, args);
        timer = null; // 执行完后清空定时器
      }, delay);
    }
  }
}

时间戳版-闭包

function throttle (fn, delay) {
  // 声明前一个时间戳
  let prev = Date.now();

  return function () {
    let _this = this;
    let args = arguments;

    // 声明当前时间戳
    let now = Date.now();
    if (now - prev >= delay) { // 如果时间戳的差大于等于时间间隔,则执行传入的函数(使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 delay 秒之后才执行一次,并且最后一次触发事件不会被执行)
      fn.apply(_this, args);
      prev = Date.now(); // 执行完后设置前一个时间戳
    }
  }
}

应用场景对比

防抖

  • 1.每次 resize/scroll 触发统计事件
  • 2.文本输入的验证,连续输入文字后发送Ajax请求进行验证,验证一次就好

节流

  • 1.DOM元素的拖拽功能实现
  • 2.搜索联想
  • 3.计算鼠标移动的距离
  • 4.canvas模拟画布功能
  • 5.射击游戏的mousedown/keydown事件-单位时间内只能发射一颗子弹
  • 6.监听滚动事件判断是否到页面底部自动加载更多

总结

函数防抖:将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。