面试官:防抖节流你会不?挑一个?

252 阅读5分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

面试官:请介绍下防抖和节流--

我:防抖是单位时间内再次触发事件时,会重新开始计时,保证单位时间内事件只执行一次,节流是单位时间内不管触发多少次事件,只执行一次,两个都可以用来节约性能

面试官:代码撸一下?

我:没问题啊

前言

听到这个的时候心里是狂喜的,这个多简单,这个我会啊,难不倒我

第一版(这是当时我写的)

  • 💥防抖
function debounce (func) {
  let timer = null
  
  return function () {
    clearTimeout(timer)
    
    timer = setTimeout(() => {
      func.call(this, arguments)
    }, 1000)
  }
}

创建一个标记用来存放定时器的返回值

再次触发事件时,把前一个定时器清除

根据定时器,每隔一段时间触发,反复触发又会重新计时

  • 💥节流
function throttle (func) {
  
  let currentState = true
  return function () {
    
    if (!currentState) {
      return
    }
    
    currentState = false

    setTimeout(() => {
      func.call(this, arguments)
      
      currentState = true
    }, 1000)
  }
}

通过保存一个标记,表示可否触发的状态

判断状态不为true 禁止运行

将 当前currentState 设置为 false,一段时间内不在触发

执行完事件后,再将这个标志设置为 true

🌰

难不倒我!

面试官扫了眼我的代码,看着我认真的说:老铁没毛病,还有补充嘛?

我:(?????一万个问号)没有了吧

内心是瞬间就紧张起来了,我觉得这里有坑,但是又不知道自己这个有什么问题,面试就这样结束了,回来后我查阅了防抖和节流的资料,总结了四个字:才疏学浅

防抖和节流

在开发过程中,我们经常会遇到各种场景中需要触发一些事件,比方说click,change,input,又或者是mousemove,scroll等,可能这些事件会频发触发,又或者有些调皮的用户在不知情的情况下间接频繁触发,事件处理函数就会被不停的调用

而更多的是,我们的页面是通过ajax请求数据渲染的,如果频繁的建立连接,服务器需要重复的响应结果,数据量很大的情况下呢?这里有人会说,缓存不就可以了,👍,确实听上去没毛病。那我们可不可以在触发事件时就减小压力呢?

✍️防抖

上面第一版代码没有毛病,平时我也是这么写的,简单方便,但是后来回忆起来,当时面试官是希望我能写的更加完美吧,错过了,/(ㄒoㄒ)/~~

function debounce (func, wait, immediate) {
  let timer
  return function () {
    const _this = this
    const args = arguments

    if (timer)clearTimeout(timer)
    if (immediate) {
      const callNow = !timer
      timer = setTimeout(function () {
        timer = null
      }, wait)
      if (callNow) func.call(_this, args)
    } else {
      timer = setTimeout(function () {
        func.apply(_this, args)
      }, wait)
    }
  }
}

我们来一步步分析

首先,参数,func:表示执行函数,wait:需要等待的事件,immediate,第三个参数表示会不会立即执行,多长时间后执行,比方说输入框每次输入是否隔规定时间触发还是立即触发

和之前一样,timer用来存放定时器的返回值

执行函数内部this的指向,否则定时器的回调中this指向window而不是事件的调用者,事件对象e指向同理

立即执行时,仅仅改变指向你会发现事件依旧被频繁调用,这里我们又需要一个指针,将callNow与timer关联取反,通过对timer变量取反,判断是否执行,通过对延迟timer变量置空来设置下一次的立即执行

再次触发前清空上一次的定时器结果,重新计时

根据对定时器执行结果的判断和是否立即触发事件的条件,确保,当事件触发之后,约定单位时间之后,执行里面的代码,如果在单位时间内再次触发了事件,那么要重新计时,以保证事件里面的代码只执行一次

✍️节流

重点补充下节流,第一版考虑的情况较少,这里我会根据时间戳还有定时器的结果来处理节流

时间戳版

function throttle (func, wait) {
  let previousTime = 0

  return function () {
    const _this = this
    const args = arguments

    const currentTime = new Date().valueOf()

    if (currentTime - previousTime > wait) {
      func.call(_this, args)
      previousTime = currentTime
    }
  }
}

根据之前的时间戳和现在的时间戳差值,跟设定的单位之间相比,大于则执行函数,后更新旧的时间戳。注意,此时因为初始时间是0,一开始就会执行,但是最后一次,是没有延迟执行的,所以可以理解成第一次触发,最后一次不触发

定时器版

function throttle (func, wait) {
  let timer

  return function () {
    const _this = this
    const args = arguments

    if (!timer) {
      timer = setTimeout(function () {
        func.call(_this, args)
        timer = null
      }, wait)
    }
  }
}

保存定时器的返回值,第一次延迟触发,给定单位时间后触发,即第一次不触发,最后一次触发

👷‍综上

function throttle (func, wait) {
  let timer
  let previousTime = 0
  
  return function () {
    const _this = this
    const args = arguments
    const currentTime = new Date().valueOf()
    
    if (currentTime - previousTime > wait) {
      if (timer) {
        clearTimeout(timer)
        timer = null
      }
      func.call(_this, args)
      previousTime = currentTime
    }
    
    if (!timer) {
      timer = setTimeout(function () {
        previousTime = new Date().valueOf()
        func.call(_this, args)
        timer = null
      }, wait)
    }
  }
}

再来!

timer表示定时器的返回值,触发事件时,取出当前时间戳,默认为0

获取当前的时间戳

(大于设置的时间,执行,否则不执行)

判断,如果现在的时间和旧时间差值大于我们设置的等待时间,即立即执行,判断定时器有值清空定时器并置空,然后立即执行,此时将当前的时间定位下一次的旧值

定时器有值的时候,触发定时器计时,间隔时间后才能触发,设置旧时间并置空定时器保证下一次能继续执行

根据时间戳实现第一次会立即触发执行函数,根据定时器实现最后一次离开后即使时间未到规定时间,因为会延迟执行一次定时器,依旧触发最后一次。

当事件触发之后,约定单位时间之内,事件里面的代码最多只能执行 1 次,所以说,所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数,节流会减少函数的执行频率

😝结语

写的不好,有错误的地方烦请各位大佬多多指教,感恩家人感恩🙏

🚩祝各位一路顺风,顶峰相见