一文教你手写完整版防抖节流

915 阅读6分钟

防抖的介绍

防抖在开发中的应用场景很多,比如在实现input输入框模糊搜索时,我们要考虑到用户的体验和服务器的压力,不能用户输入一个字母就发送一个请求;或者是用户在点击提交按钮的时候,连续点击,就会产生多条记录。这时,防抖函数起到了很大的作用。

防抖的原理

 重复调用同一个函数时,在用户设置的延迟时间内,会取消之前的调用,只会执行最后一次调用。

防抖的实现

首先,在text.html中简单的写个输入框,监听用户的输入

<body>
<input type="text">
<script src="./debounce.js"></script>
<script>
  const input = document.querySelector('input')
  function handleInput(event) {
   console.log('发送网络请求:',event.target.value)
  }
  input.oninput = handleInput
</script>
</body>

test.gif

好了,一个简单的用户输入,模拟模糊搜索就完成了,可以看到请求接口很频繁。接下来,我们一起实现防抖函数吧。

思考

  1. 怎么在用户连续输入的时候取消上次的函数调用呢?

    我们可以实现一个函数,函数的参数接收用户要执行的函数和用户希望在多少时间内不重复执行传入的函数。然后定义一个变量来维护一个定时器,如果在用户希望的延迟时间内调用,则会清除定时器。这样最后只有一个定时器会执行。 好了,我们来实现一下吧。


// 防抖函数
function debounce(fn, delay = 1000){
 let timer 
 function _deounce() {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  if(timer){
   clearTimeout(timer)
  }
  timer = setTimeout(() => {
   fn()
  },delay)
 }
 return _deounce
}
// html
 const input = document.querySelector('input')
 function handleInput() {
  console.log('发送网络请求')
 }
const _debounce = debounce(handleInput, 2000)
input.oninput = _debounce

让我们来看一下效果吧

debounce1.gif

还有个问题,就是this指向和参数为题。

  1. this指向window
  2. 不能实现传递参数

比如,我们在handleInput函数中输出一下this和event


 const input = document.querySelector('input')
 function handleInput(event) {
  console.log('发送网络请求')
  console.log(this)
  console.log(event)
 }
const _debounce = debounce(handleInput, 1000)
 input.oninput = _debounce

image.png

这个问题很简单,改变this的方式很多,比如bind,apply,call

// 防抖函数
function debounce(fn, delay = 1000){
 let timer
 function _deounce(...args) {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  if(timer){
   clearTimeout(timer)
  }
  timer = setTimeout(() => {
   fn.apply(this, args)  // 改变this和传递参数
  },delay)
 }
 return _deounce
}

image.png

好了,this的指向和参数都正常了,这就实现了一个简单的防抖。但是这也叫完整版吗?当然不是, 我们在完善一下功能。

立即执行

有一些情况是在用户输入第一个字符时就发一次请求,最后发一次请求。那我们怎么实现呢

  1. 防抖函数新增参数immediate,true代表要立即执行,false代表不立即执行

有了这个思考,可能有很多小伙伴已经有了实现方法。

// 防抖函数
function debounce(fn, delay = 1000, immediate){
 let timer
 function _deounce(...args) {
  // 是否立即执行
  if(immediate){ 
   fn.apply(this, args)
   immediate = false
  }
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  if(timer){
   clearTimeout(timer)
  }
  timer = setTimeout(() => {
   fn.apply(this, args)
  },delay)
 }
 return _deounce
}

但是这有一个问题,在第二次输入时,并不会立即执行,因为在第一次立即执行后,传入的immediate被改为false了。我们需要改进一下代码。

  1. 利用闭包,维护一个全局变量isStarted,这个代表是否立即执行过,默认为false
  2. 在立即执行后设置为true,在定时器中执行回调后设置为false。
// 防抖函数
function debounce(fn, delay = 1000, immediate){
 let timer
 let isStarted = false
 function _deounce(...args) {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  if(immediate && !isStarted){
   fn.apply(this, args)
   isStarted = true
  }
  if(timer){
   clearTimeout(timer)
  }
  timer = setTimeout(() => {
   fn.apply(this, args)
   isStarted = false
  },delay)
 }
 return _deounce
}

这样就没问题了,让我们看一下实现的效果吧

debounce3.gif

函数返回值

当我们的要执行的函数有返回值时,我们怎么拿到值呢? 比如一下情况

 const input = document.querySelector('input')
 function handleInput(event) {
  console.log('发送网络请求')
  console.log(this)
  console.log(event)
   return 'aaaa'
 }
 const _debounce = debounce(handleInput, 2000, true)
 input.oninput = _debounce

实现方法:

  1. 用户传入一个回调函数,在定时器或者立即执行时拿到返回值,调用回调函数,暴露返回值
  2. 使用Promise来暴露函数返回值

首先实现一下回调函数

// html
 const input = document.querySelector('input')
 function handleInput(event) {
  console.log('发送网络请求')
  console.log(this)
  console.log(event)
   return 'aaaa'
 }
const _debounce = debounce(handleInput, 2000, true, (res) => {
 console.log('回调函数拿到的值:',res)
 })
 input.oninput = _debounce


// 防抖函数
function debounce(fn, delay = 1000, immediate, callback){
 let timer
 let isStarted = false  // 是否立即执行过
 function _deounce(...args) {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  if(immediate && !isStarted){
   const value =  fn.apply(this, args)
   callback(value)
   isStarted = true
  }
  if(timer){
   clearTimeout(timer)
  }
  timer = setTimeout(() => {
   const value =  fn.apply(this, args)
   callback(value)
   isStarted = false
  },delay)
 }
 return _deounce
}

轻松秒杀,看一下结果

image.png

其次,用promise实现一下

思考

  1. 我们怎么拿到返回的promise
  2. 怎么执行then回调

解决办法: 在oninput的时候执行我们定义的函数,在定义的函数内执行_debounce函数,这样就可以拿到promise,然后快乐的调用then回调了

代码实现

 const input = document.querySelector('input')
 function handleInput(event) {
  console.log('发送网络请求')
  console.log(this)
  console.log(event)
   return 'aaaa'
 }

const _debounce = debounce(handleInput, 2000, true, (res) => {
 console.log('回调函数拿到的值:',res)
 })

 const tempFn = function (...args) {
   _debounce.apply(this, args).then(res => {
    console.log('then中拿到值:',res)
   })
 }

 input.oninput = tempFn

// 防抖函数
function debounce(fn, delay = 1000, immediate, callback){
 let timer
 let isStarted = false  // 是否立即执行过
 function _deounce(...args) {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  return new Promise((resolve,reject) => {
   if(immediate && !isStarted){
    const value =  fn.apply(this, args)
    resolve(value)  // 使用resolve传值
    isStarted = true
   }
   if(timer){
    clearTimeout(timer)
   }
   timer = setTimeout(() => {
    const value =  fn.apply(this, args)
    resolve(value)
    isStarted = false
   },delay)
  })
 }
 return _deounce
}

如果对promise不是很理解的话,可以看我上一篇文章手写promise

取消功能

在用户输入完成后,如果用户直接点击了提交,或者是切换了页面,那么最后一次也不需要请求了。 实现如下

// 防抖函数
function debounce(fn, delay = 1000, immediate, callback){
 let timer
 let isStarted = false  // 是否立即执行过
 function _deounce(...args) {
  // 利用闭包,存储timer
  // 当有定时器时,清除定时器
  return new Promise((resolve,reject) => {
   if(immediate && !isStarted){
    const value =  fn.apply(this, args)
    resolve(value)
    isStarted = true
   }
   if(timer){
    clearTimeout(timer)
   }
   timer = setTimeout(() => {
    const value =  fn.apply(this, args)
    resolve(value)
    isStarted = false
   },delay)
  })
 }

 // 取消
 _deounce.cancel = function (){
  if(timer){
   clearTimeout(timer)
   timer = null
  }
 }
 return _deounce
}

好了,一个完整的防抖函数就实现了,接下来让我们实现节流函数吧。

节流的介绍

想必大家都玩过lol吧,当uzi在使用vn时,疯狂的右击敌方英雄。鼠标十秒内可能被点击了100次,但是英雄只进行了十次普通攻击,这就是节流的应用。

节流的原理

如果持续触发某个事件,则每隔n秒执行一次

节流的实现

思考

  1. 我们怎么判断距离上次调用的时间小于用户设定的时间?

    可以维护一个全局变量lastTime,用来存储上一次执行的时间

我们还是用input输入框来模拟。

// html
const input = document.querySelector('input')
function handleInput(event) {
 console.log('发送网络请求')
 console.log(this)
 console.log(event)
 return 'aaaa'
}

const _throttle = throttle(handleInput, 1000)

input.oninput = _throttle

function throttle(fn, interval) {
 let timer
 let lastTime = 0  // 存储上一次执行的时间
 function _throttle(...args) {
  let nowTime = new Date().getTime()  // 获取函数执行时间
  let remainTime = interval - (nowTime - lastTime) // 剩余时间
  // 如果当前执行函数与上一次执行的时间差大于用户设定的,则应该开启定时器
  if(remainTime <= 0){ 
   lastTime = nowTime
   fn.apply(this,args)
  }
 }

 return _throttle
}

看看效果吧

throttle1.gif

首次执行和尾执行

我们要完善节流函数的功能,比如根据用户传递的参数,是否开启首次执行或者是否开启尾执行

  1. 首次执行

这个功能我们已经实现了,默认的lastTime是0,则第一次会执行。我们只要根据用户传入的值来设置lastTime就可以


function throttle(fn, interval,options = {leading: true}) {
 let timer
 let lastTime = 0  // 存储上一次执行的时间
 function _throttle(...args) {
  let nowTime = new Date().getTime()  // 获取函数执行时间
  
 // 当不开启首次执行时
 if (lastTime === 0 && options.leading === false) {
  lastTime = nowTime
 }

  let remainTime = interval - (nowTime - lastTime) // 剩余时间
  // 如果当前执行函数与上一次执行的时间差大于用户设定的,则应该开启定时器
  if(remainTime <= 0){
   lastTime = nowTime
   fn.apply(this,args)
  }
 }

 return _throttle
}
  1. 尾执行

我先解释一下什么是尾执行。尾执行就是在用户最后一次输入之后,距离上一次执行的时间差之后,执行一次函数。

function throttle(fn, interval, options = {leading: true, trailing: true}) {
 let timer
 let lastTime = 0// 存储上一次执行的时间
 function _throttle(...args) {
  let nowTime = new Date().getTime()  // 获取函数执行时间

  if (lastTime === 0 && options.leading === false) {
   lastTime = nowTime
  }

  let remainTime = interval - (nowTime - lastTime) // 剩余时间
  // 如果当前执行函数与上一次执行的时间差大于用户设定的,则应该开启定时器
  if (remainTime <= 0) {
   lastTime = nowTime
   fn.apply(this, args)

  }


 
 // 是否是开启执行
  if(remainTime > 0 && options.trailing) {
   if(timer){
    clearTimeout(timer)
    timer = null
   }
   timer = setTimeout(() => {
    fn.apply(this,args)
    lastTime = options.leading ? 0 : new Date().getTime()
   },remainTime)
  }
 }

 return _throttle
}

我们看一下实现效果

throttle1.gif

剩余函数返回值和取消功能实现和防抖函数一致,就不再演示。

结尾

如果感觉对你有帮助,请点个赞吧,感谢。