【JavaScript】防抖、节流原理以及使用场景

0 阅读6分钟

函数防抖--Debounce

事件被触发后n秒再执行回调函数,如果在n秒内再次被触发,则重新计时

先来感受一下防抖的作用

没有防抖的代码示例:

function search (content) {
  console.log('查询' + content);
}
    
let input = document.querySelector('#input')

input.addEventListener('keyup', function (e) {
  search(this.value)
})

运行结果:

未作防抖的效果

可以发现每次我们键盘输入的后一瞬间,就会发送查询请求。这不仅非常浪费资源而且在实际应用中,我们查询时是需要全部输入结束后才想要请求返回结果。

优化一下:

// 防抖函数
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {  // 获取事件函数的所有参数
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  }
}

let input1 = document.querySelector('#input1')
input1.addEventListener('keyup', debounce(function() {
  search(this.value)
}, 500))

运行结果:

防抖后的效果

加了防抖后可以看到在频繁触发事件时不会发送请求查询。

在指定间隔时间内没有触发事件,才会执行函数,在间隔期间再次触发事件则会重新计时。

防抖的原理

当事件首次触发时,会设置一个定时器,如果在设定的时间内事件被再次触发,则会清除之前的计时器并重新设置新的计时器只有时间在设定的延迟时间内没有被再次触发时,函数才会执行

接下来我们来模拟一下这个过程:

let sig = function () {
  console.log('间隔时间内未重复触发事件', new Date().toLocaleString())
}

let mul = function () {
  console.log('间隔时间内重复触发事件', new Date().toLocaleString())
}

setInterval(debounce(sig, 500), 1000) // 规定防抖间隔 0.5秒,1秒触发一次事件
setInterval(debounce(mul, 2000), 1000)  // 规定防抖间隔2秒,1秒触发一次事件

运行结果:

防抖本质测试

可以发现,只有sig函数被正常执行。其实是因为在规定的防抖间隔时间内sig函数没有被反复触发,正常执行。而mul函数在间隔时间内被反复触发重置计时所以没法执行输出。

  • 时间线分析:

    • 0ms: 启动setInterval,安排1000ms后调用闭包函数
    • 1000ms: 调用闭包函数,设置500ms的定时器
    • 1500ms: 定时器触发,执行sig函数,输出 "间隔时间内未重复触发事件"
    • 2000ms: 再次调用闭包函数,设置新的500ms定时器
    • 2500ms: 定时器触发,再次输出 "间隔时间内未重复触发事件"
    • ... 以此类推
  • mul函数为什么不执行

对于setInterval(debounce(mul, 2000), 1000),因为2000ms > 1000ms,所以每次定时器尚未触发就被下一次调用清除了,导致mul函数不会执行。

可以把函数的防抖理解为moba游戏的回城操作,如果你在回城期间(防抖间隔)反复点击按钮,则进度条会反复重置,直到最后一次点击

函数节流--Throttle

节流的原理

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

来个代码感受一下:

// 节流
function throttle (fn, wait) {
  let preTime = 0
  return function (...arg) {
    let nowTime = Date.now()  // 第一次点击的时间戳
    if (nowTime - preTime >= wait) {
      // fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表  apply方法显式绑定this
      fn.call(this, ...arg)  // call 数组解构,直接传入参数列表  显式绑定this指向
      preTime = nowTime
    }
  }
}

运行效果:

节流效果

可以看到,持续输出的情况下,节流会在一定时间间隔后执行一次目标函数

可以把节流理解为攻速阈值,在规定的数值内,不论你敲键盘按平A有多频繁,都只会规定时间后发出普攻,这就是节流。

防抖、节流的实现方法

一、lodash实现方法

1. 防抖:_.debounce()

<body> ...... </body>
<script src="./lodash.min.js"></script>  <!-- 引入lodash库-->

<script>
// DOM操作内容
box.addEventListener('mousemove', _.debounce(mouseMove, 500))
</script>

2. 节流:_.throttle()

<body> ...... </body>
<script src="./lodash.min.js"></script>  <!-- 引入lodash库-->

<script>
// DOM操作内容
box.addEventListener('mousemove', _.throttle(mouseMove, 1000))
</script>

二、万能的手搓

1. 防抖

  1. 触发事件时,清除之前设置的定时器(如果有)
  2. 启动新的定时器,在指定的时间间隔内等待
  3. 在等待期间再次触发事件,重复步骤1、2
  4. 如果等待期间没有触发事件,执行函数
防抖代码思路
  1. 定义防抖函数,传入参数为需要执行的函数引用和延迟时间
  2. 由于可能需要定位且清除定时器,考虑到函数执行后被销毁,使用闭包实现
  3. 防抖函数返回的需要是函数体供事件触发调用
// 防抖函数
function debounce (fn, t) {
  let timeId
  // 返回一个匿名函数
  return function () {
    // 如果有定时器就清除
    if (timeId) clearTimeout(timeId) // 使用闭包,定时器就不会被销毁
    // 开启定时器 500
    timeId = setTimeout(function () {
      fn()
    }, t)
  }
}
发现问题
  1. 大部分情况,函数传递不止参数且有一个事件参数e中存放着可能需要的方法
  2. this指向被覆盖丢失,会导致后续对DOM元素使用this操作时出错
解决办法
  1. 使用ES6函数拓展方法和数组解构...arg将参数全部传递给arg数组
  2. 使用callapply方法显式绑定this
  3. bind返回值为一个函数,在此处不适用
防抖最终代码
function debounce (fn, wait) {
  var timer = null
  return function (...arg) {  // 函数this指向btn,扩展运算符,将参数转为数组,用于传递参数
    let that = this  // 在定时器为非箭头函数时需要
    clearTimeout(timer)
    timer = setTimeout(function () {
      fn.call(that, ...arg) // 数组解构,args盛放参数列表  call方法显式绑定this
    }, wait)
  }
}


function debounce (fn, delay) {
  let timer = null;
  return function (...args) { // 扩展运算符,将参数转为数组,用于传递参数
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表  apply方法显式绑定this
    }, delay);
  }
}

2. 节流

  1. 初始化检查当前有没有正在等待的定时器
  2. 首次触发时,创建定时器,设置间隔时间后执行函数
  3. 等待期间再次触发,检查定时器是否还在运行
  4. 定时器到期执行目标函数,将 定时器重置,准备接受下一次触发
  5. 回到步骤1,准备处理下一个时间窗口的触发

节流遇到的问题与防抖同理,这里不过多赘述

节流最终代码
function throttle (fn, wait) {
  let preTime = 0
  return function (...arg) {
    let nowTime = Date.now()  // 第一次点击的时间戳
    if (nowTime - preTime >= wait) {
      // fn.apply(this, args); // 不需要数组解构,args数组,盛放参数列表  apply方法显式绑定this
      fn.call(this, ...arg)  // call 数组解构,直接传入参数列表  显式绑定this指向
      preTime = nowTime
    }
  }
}

防抖、节流应用场景

防抖应用场景

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口会不断的触发这个事件,用防抖来让其只触发一次
  • 网络状态不佳时,用户多次快速点击按钮,用防抖只触发一次,降低服务器端的并发压力

节流应用场景

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断