带你“深入”防抖

简介: 事件响应函数在一段规定时间(前/后)才执行。如果在规定时间内,再次触发,重新计算时间。


网络异常,图片无法展示
|


说白话:


抖是什么?它啊,就像大炮,投一个炸弹,装一个炸弹。那个函数啊,触发一次就执行一次。


那么,防抖又是什么?就像机关枪,突突突,不管打多少次,打完子弹仓里都要重新装子弹。高频触发函数,时间间隔会重新计算。当在最后一次触发函数时(最后一个子弹打完),时间到达执行一次。


网络异常,图片无法展示
|


说人话:


事件响应函数在一段规定时间(前/后)才执行。如果在规定时间内,再次触发,重新计算时间。


初模样:


<div class="box"></div>
<button id="btn">取消防抖</button>
<script>
  let obox = document.querySelector('.box')
  let count = 0
  obox.innerHTML = count
  obox.onmousemove = function () {
    obox.innerHTML = count++
    console.log(count);
  }
</script>

网络异常,图片无法展示
|
当鼠标移动n次,就会触发n次。


整改模样:


// <script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script> 
// 或
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
let obox = document.querySelector('.box')
let count = 0
obox.innerHTML = count
function todo(e) {
  obox.innerHTML = ++count
  console.log(e);
}
obox.onmousemove = _.debounce(todo, 1000)
</script> 


直接使用lodash.js或者underscore.js中的防抖函数,就可以做到1s内,鼠标疯狂移动只触发一次。


网络异常,图片无法展示
|


造个模样


对于我们而言,光知其然,是远远不够的;我们更要知其所以然!

二话不说,咱们就来凭空捏造一个把!


就underscore而言,先剖析这个debounced(防抖动)函数。它有三个参数:防抖动的函数fun、需要延迟的毫秒数wait、是否立即执行immediate。


第一版


先照葫芦画瓢,把形参先整好。最先在鼠标移动时,它接收的是一个函数,所以需要返回一个函数;其次,需要等待规定时间内执行,需要一个定时器。


function debounce(fn, wait = 200, immediate = false) {
  let timer = null
  return function () {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn()
    }, wait)
  }
}

可以使用setTimeout定时器,将功能函数在一定时间内执行一次。这样最基础的防抖函数就🆗拉!


第二版


我们不光需要考虑功能函数,还需要考虑到在执行函数功能时,fn函数中可能使用event事件、内部this指向问题。此外第一版只完成了后执行,我们还需要完成立即执行的功能。


let obox = document.querySelector('.box')
let count = 0
obox.innerHTML = count
function todo(e) {
  obox.innerHTML = ++count
  console.log(this, e);
}
obox.onmousemove = _.debounce(todo, 1000,true)
// <div class="box">1</div>
// MouseEvent{isTruted: true, screenX: 87, screenY: 388, clientX: 68, clientY: 295,...}


在使用我们第一版的this指向的是window,并且e为undefined。

在自定义debounce函数中,我们发现返回的函数this指向div,这时我们就需要在fn函数执行时,改变this指向。


考虑参数传递问题,在返回函数中接收参数,在函数执行时传入参数即可。

function debounce(fn, wait = 200, immediate = false) {
  let timer = null
  return function (...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}


此外,我们还需要考虑是否立即实行,及第三个参数。


如果传入的参数immediate为true,那么就执行fn函数;如果为false的话,那就需要在一定时间之后执行(使用setTimeout)。


使用immediate来判断是否立即执行:当立即执行时,此时必须没有定时器,执行函数。等待2s,将定时器清空,等待执行下一次。


function debounce(fn, wait = 200, immediate = false) {
  let timer = null, result
  return function (...args) {
    if (timer) clearTimeout(timer)
    if (immediate) {// 立即执行
      (!timer) && fn.apply(this, args)  // 一开始就执行,无定时
      timer = setTimeout(() => {
        timer = null
      }, wait)
    } else {// 后执行
      timer = setTimeout(() => {
        fn.apply(this, args)
      }, wait)
    }
  }
}

此外还可以通过变量存储,记录执行顺序。

function debounce(fn, wait = 200, immediate = false) {
  let timer = null
  let isEnd = true // 默认后执行
  return function (...args) {
    if (timer) clearTimeout(timer)
    if (immediate) { // 先执行
      isEnd && fn.apply(this, args)
      isEnd = false
    }
    timer = setTimeout(() => {
      (!immediate) && fn.apply(this, args) // 后执行
      isEnd = true
    }, wait)
  }
}


第三版


在第二版的基础上我们可以添加函数返回值和取消抖动的方法。


添加函数返回值,可以记录执行函数的值,不管是立即执行还是后执行,最后统一返回这个值。


function debounce(fn, wait = 200, immediate = false) {
  let timer = null, isEnd = true, result
  let debounced = function (...args) {
    if (timer) clearTimeout(timer)
    if (immediate) {
      isEnd && (result = fn.apply(this, args))
      isEnd = false
    }
    timer = setTimeout(() => {
      (!immediate) && (result = fn.apply(this, args))
      isEnd = true
    }, wait)
    return result
  }
  return debounced
}


使用result记录返回值,最后返回即可。上述代码做了一点点小改动,将整个返回函数使用变量记录,将该变量返回。这样方便于接下来,给函数添加取消抖动的方法。


function debounce(fn, wait = 200, immediate = false) {
  let timer = null, isEnd = true, result
  let debounced = function (...args) {
    if (timer) clearTimeout(timer)
    if (immediate) {
      isEnd && (result = fn.apply(this, args))
      isEnd = false
    }
    timer = setTimeout(() => {
      (!immediate) && (result = fn.apply(this, args))
      isEnd = true
    }, wait)
    return result
  }
  debounced.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
  }
  return debounced
}


在cancel方法中,直接清除抖动的定时器,并将该变量回收。


函数返回值异步问题


很感谢读者提的建议,我使用underscore后发现,确实接收的返回值存在异步问题。


let obox = document.querySelector('.box')
let obtn = document.querySelector('#btn')
let count = 0
function todo(e) {
  obox.innerHTML = ++count
  console.log(this, e);
  return count
}
let debounceFn = _.debounce(todo, 1000, false)
obox.onmousemove = (e) => {
  let value = debounceFn(e)
  console.log(value);
}


当我第一次进入div时,执行一次todo函数,此时返回值count应该为1,但是实际输出为undefined。第二次进入的时候,输出为1,但是页面的count为2。返回值返回的是上一个返回值。


网络异常,图片无法展示
|
为解决异步问题,我们可以使用promise来解决。

function debounce(fn, wait, immediate) {
  let timer = null, result
  let debounced = function (...args) {
    return new Promise(res => {
      if (timer) clearInterval(timer)
      if (immediate) {// 立即执行
        if (!timer) {
          result = fn.apply(this, args)
          res(result)
        }
        timer = setTimeout(() => {
          timer = null
        }, wait);
      } else {
        timer = setTimeout(() => {
          result = fn.apply(this, args)
          res(result)
        }, wait);
      }
    })
  }
  debounced.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
  }
  return debounced
}
let obox = document.querySelector('.box')
let obtn = document.querySelector('#btn')
let count = 0
function todo(e) {
  obox.innerHTML = ++count
  console.log(this, e);
  return count
}
let debounceFn = debounce(todo, 1000, false)
obox.onmousemove = async (e) => {
  try {
    let value = await debounceFn(e)
    console.log(value);
  } catch (e) {
    console.log(e);
  }
}


使用promise解决返回值异步问题,在调用时,使用async/await,将其同步。进入div,调用一次,输出值为1,调用两次,输出值为2,返回值同步。

网络异常,图片无法展示
|


有什么用


防抖最常见的应用莫过于解决频繁访问接口的问题了。

总结一下常见的应用:


  • 防止表单多次提交
  • 搜索框输入查询(监听输入框输入内容,设定每隔一段时间访问接口)
  • scroll滚动触发
  • 浏览器窗口缩放时,resize事件


回顾


防抖函数中牵扯到apply改变this绑定、闭包等知识点。我们可以对一下文章做个回顾:


目录
相关文章
|
6月前
节流、防抖
节流、防抖
44 2
|
6月前
|
前端开发 UED
关于防抖和节流的理解
防抖和节流是前端开发中常用的两种性能优化技术。
36 0
防抖&节流
防抖&节流
116 0
什么是防抖和节流,怎么实现一个防抖和节流?
功能:当事件被触发N秒之后再执行回调,如果在N秒内被触发,则重新计时。
|
JavaScript 前端开发
节流与防抖
节流与防抖
51 0
|
前端开发
什么是防抖和节流?有什么区别?
什么是防抖和节流?有什么区别?
78 0
|
JavaScript 前端开发
什么是防抖和节流?如何实现防抖和节流?
防抖(debounce)和节流(throttle)是 JavaScript 中常用的两种性能优化方法。
|
前端开发 JavaScript
关于防抖和节流我所知道的
关于防抖和节流我所知道的
59 0