「查漏补缺」手写节流throttle和防抖debounce

511 阅读5分钟

之前写异步JS时不小心把定时器给遗漏了,作为异步函数,有时候我们优化性能,减轻页面压力需要用到定时器,然后在查阅文档时也发现了自己的一些不足,于是尽量在这篇文章上补齐,方便自己以后能回顾。

setTimeout()

当delay秒之后执行function函数

参数

  • function 当定时器结束,会自动执行这个函数
  • code 可以使用字符串代替函数(不推荐这种语法)
  • delay 延时(单位:毫秒),如果这个参数是0,可以省略
  • args 参数,这个参数会传递给function

示例:

setTimeout((...args)=>{
   console.log(args)
},1000,1,2,3,4,5)
// 1秒钟后结果:[1,2,3,4,5]

// 不建议的操作
setTimeout('console.log(123)',1000)  //现在谷歌已经拒绝这样的用法了

setTimeout()执行后会返回一个id,这个结果是定时器的编号,通过这个编号我们可以取消定时器

let timer=setTimeout(()=>{console.log(123)})
clearTimeout(timer)

定时器与this

需要注意的是定时器会将函数内部的运行环境设置为全局运行环境,也就是指向window

var x= 1
var obj={
   x:2,
   y(){console.log(this.x)}
}
setTimeout(obj.y,1000) //2

在obj.y当成参数时,会隐式丢失函数内部的this,所以上面的打印结果为2

解决方法:

1、传递一个函数,让obj.y在函数内部执行

setTimeout(()=>{obj.y()},1000)

2、使用bind绑定

setTimeout(obj.y.bind(obj),1000)

setInterval()

这个函数跟setTimeout的用法基本一样,只不过它是隔一段时间执行,就相当于每隔一个delay执行function。

注意:这个函数是多少秒之后开始执行,它不会管执行的时间。假设一个任务是定时100ms执行一个函数,而这个函数执行需要花费5ms,那么第一次之后定时器都会在任务执行后的95ms后执行,所以时间方便不会很精确。

所以一般我们使用setTimeout来模拟setInterval

var timer=null
let count =0
function fn(){
   clearTimeout(timer)
   timer = setTimeout(()=>{
      count +=1  
      console.log(count)
      fn()
   },1000)
   if(count ===10){
     clearTimeout(timer)}
}

上面这段代码我尽可能想每一次都清除前一个定时器,目前代码运行没问题,如果您有更好的想法,请贴到下方评论区。

防抖debounce

看了网上很多人的防抖代码,一上来就贴全部代码,实在看不懂,我在理解了防抖概念后,写了一个简单版防抖并做了封装。如果您有更好的想法和思路,请贴到下方评论区教教我。

思路

我目前对防抖的概念就是使用清除定时器来对用户的操作进行限制,相当于一个cd条,如果打断了,就不执行(清除定时器)。

比如我现在要做一个button按钮,点击两秒钟后打出一句123,说干就干。

let btn=document.querySelector('button')
btn.addEventListener('click',()=>{
  setTimeout(()=>{console.log(123)},1000)
})

没有防抖前效果如下 简单做个防抖

let btn=document.querySelector('button')
let timer=null
btn.addEventListener('click',()=>{
  clearTimeout(timer) //每点一次都清除上一次的timer,上一次的就不会执行
  timer=setTimeout(()=>{console.log(123)},1000)
  console.log(timer)
  //这句代码是为了证明获取timer比console早
  //只要执行了setTimeout就拿到timer了,不用执行参数函数console
})

效果如下:

优化代码

let btn=document.querySelector('button')

function debounce(handler,delay){//delay就是1000ms
   let timer=null
   return function (){
      clearTimeout(timer)
      timer=setTimeout(handler,delay)
   }
}
const handler=()=>{console.log(123)} 
btn.addEventListener('click',debounce(handler,1000))

优化代码思路:

逻辑是基于上面的简化版进行的函数封装,就是使用闭包,把timer包起来。

然后按照里面的内容拆开写debounce的参数,以上的参数是处理函数handler(就是console.log(123))和delay(时间),如果有更多的业务逻辑,就写更多的参数做封装就行了。

节流throttle

一开始搞错了节流的概念,看着网友们的描述,还以为节流是个循环函数。

后来发现节流实际上就是不管用户点多少次,我只执行第一次。

节流与防抖的区别

它跟防抖的关系好比一个法师在施法,如果这个法师不断在施法,但是它的技能是可以被打断的,每打断一次都重新触发施法那就是防抖。

如果这个法师的技能不能被打断,不管你打了多少次,法师都能把刚开始的施法读条delay给做完,这就是节流。

示例

没有节流的效果

思路:如何让上面的代码在我设置的时间范围内只跑一次呢?还是使用setTimeout实现,如果有一个开关,当开关是打开的时候,就跑代码。如果开关闭合,就不执行代码,跑代码过程中将闭合开关不就行了吗?

节流的效果 代码如下

let btn=document.querySelector('button')
const handler=()=>{console.log('初步节流')}
let toggle=true //设置一个开关
btn.addEventListener('click',()=>{
  if(!toggle){//如果开关是关着的,就return
    return null
  }
  toggle=false //此时函数正在执行,把开关闭合
  setTimeout(()=>{
    handler() //这里是处理业务逻辑了
    toggle=true //处理完之后把开关打开
  },1000)
})

闭包封装

let btn=document.querySelector('button')
const handler=()=>{console.log('闭包封装')}
function throttle(){
  let toggle=true //把toggle当做闭包
  return function (){
    if(!toggle){
      return null
    }
    toggle=false
    setTimeout(()=>{
      toggle=true
      handler()
    },1000)
  }
}
btn.addEventListener('click',throttle())

优化代码

let btn=document.querySelector('button')
const handler=()=>{console.log('封装函数')}

/*
参数:
handler: 执行函数
delay:时间
*/
function throttle(handler,delay){ 
  let toggle=true
  let timmer //定时器
  return function (){
    if(!toggle){
      return null
    }
    toggle=false
   timer= setTimeout(()=>{
     clearTimeout(timmer) //清除前一次定时器
      handler()
      toggle=true
    },delay)
  }
}
btn.addEventListener('click',throttle(handler,2000))

后话

这篇博客主要是用来记录一下今天看文档的心得,虽然不多,但是也结合了自己的思考,做个记录,如果您对上面的代码有意见,请在下方评论区帮我优化一下,不慎感激。