阅读 2066

手撸JS合集——防抖节流

前言

今天我们的话题是前端中的防抖和节流,这个话题已经是一个经典的问题了,基本面试中都会遇见,也有很多文章来写这个话题,那么今天我们也一起来盘点一下这个话题吧~

PS:现在的面试对于原生JS以及JS基础都有一定的要求,我们也要对这块有一定的重视,时常进行一定的训练,接下来咱们会出一个JS常见的手写题目的合集,每天掌握一个,手写代码不再慌!

前置

首先我们在手写代码之前,我们应该要搞清楚几个问题:

  • 什么是防抖和节流?
  • 他们有什么用?
  • 我们什么时候才会用到他们?

好啦,接下来我们就一个一个问题来解决~

什么是防抖和节流

我们先来看比较正式一点的回答: 防抖:把触发非常频繁的事件合成一次去执行,在指定的时间内只执行一次回调函数

节流:频繁触发事件时,只会在指定的时间段内执行事件回调

总结:那其实防抖和节流就是解决某个事件在短时间内被高频触发的两种不同的方案

其实好好看一下也挺好懂的,没懂也没关系,我们来举个🌰:

防抖:假设现在坐公交车,很多人不断在刷卡,那么此时司机是不能开车的,等到乘客都刷卡完毕了,司机需要等待一会儿(延迟时间)确认乘客都做好之后准备开车,此时正好又有一个乘客上车,那么司机又要重新等待刷卡换人再次坐稳之后再开车

节流:我们都知道水龙头拧紧之后,他不是完全不会出水,我们发现他每隔一段时间就会滴一点水出来

OK,仔细品味,应该还挺好理解的吧~

好了,知道了这两是干嘛的了,那接下来我们就要show my code了~

防抖

通过上面我们知道了防抖的含义,那我们是不是可以得出一个结论:如果指定时间内又触发该事件,回调函数执行时间会基于此刻重新开始计算

那我们的思路就是每次执行回调函数之前都先看看是否之前有触发,如果有咱们就清掉计时器重新开始计时,我们代码如下:

const debounce = (fn, delayTime) => {
  let timerId
  return function () {
    let th = this
    let args = arguments
    timerId && clearTimeout(timerId)
    timerId = setTimeout(() => fn.apply(th, args), delayTime)
  }
}
复制代码

其实上面这个就是防抖的最常用实现,但是我们看看代码你是否发现了一个问题?那就是他要等待延迟时间之后才会执行事件,那假如我总共就只触发了一次呢?按照这个情形的话,那岂不是就一次触发也要等待延迟了才会触发,这显然不太好。

其实防抖有两种形式:非立即执行防抖立即执行防抖,顾名思义他们的区别就在于他们的第一次是不是立即执行的,上面这个实现很显然就是非立即执行防抖,那立即执行防抖又应该怎样实现呢?

其实很简单,就改一句代码就好了:

const debounce = (fn, delayTime) => {
  let timerId
  return function () {
    let th = this
    let args = arguments
    // 说明第一次直接执行回调,反之不是,重新计时
    timerId == null ? fn.apply(th, args) : clearTimeout(timerId)
    timerId = setTimeout(() => fn.apply(th, args), delayTime)
  }
}
复制代码

清楚了定义,也知道了实现的方式,那我们是否可以想象我们日常开发中能用上的业务场景了,比如:

  • 搜索框搜索,我们只需要用户输完之后,再发送请求
  • 浏览器窗口大小变更,我们是不是可以等待变更完成之后再计算窗口大小,防止重复渲染
  • 文本编辑器的实时保存,无任何操作之后等待延迟时间进行保存
  • 比如某些应用的点赞功能,我们就需要点击之后立即生效,但是用户后续如果不停的点击我们就可以进行防抖操作

好了,接下来我们再来看看节流~

节流

节流我们通过分析是在指定时间内触发事件,那么我们同样得出一个结论:触发事件间隔大于等于指定的时间才会执行回调函数

节流有两种实现方式:定时器时间戳的方式,我们都来看看

计时器的方式:

const throttle = (fn, delayTime) => {
  let timerId
  return function() {
    let th = this
    let args = arguments
    if (!timerId) {
      timerId = setTimeout(() => {
        timerId = null
        fn.apply(th,args)
      }, delayTime)
    }
  }
}
复制代码

定时器的方式:函数并不会立即执行,而是需要等待延迟计时器完成之后才会执行,所以如果最后一次触发回调事件与前一次时间间隔小于delayTime,最后一次也会执行

那我们再来看时间戳的方式实现:

const throttle = (fn, delayTime) => {
  let _start = Date.now()
  return function () {
    let th = this
    let args = arguments
    let _now = Date.now()
    if(_now - _start >= delayTime) {
      // 时间差大于等待时间,函数可重新调用
      fn.apply(th, args)
      //更新上一次函数执行时间戳
      _start = Date.now()
    }
  }
}
复制代码

时间戳的方式:假如页面加载就开始计时,加载时间大于delayTime,第一次触发事件回调的时候便会立即执行fn,如果最后一次触发回调和前一次触发的差小于delayTime,则最后一次不会触发

那看完了节流的两种实现方式,我们发现每一种都有自己的缺点之处,但是两者之间又正好互补,那么接下来我们就将这两种方式结合起来实现更优的节流方式:

我们来看下实现代码:

const throttle = (fn, delayTime) => {
  let timerId, _start = Date.now()
  return function() {
    let th = this
    let args = arguments
    let _now = Date.now()
    let remainTime = delayTime - (_now - _start)
    if(remainTime <=0) {
      fn.apply(th,args)
      _start = Date.now()
    }else{
      clearTimeout(timerId)
      setTimeout(()=>fn.apply(th, args), remainTime) 
    }
  }
}
复制代码

我们将两者结合起来,我们发现我们便可以在第一次触发事件时执行fn,并且如果最后一次和上一次间隔比较短,那我们最后一次在延迟时间达到之后也会执行

清楚了节流的定义和实现,我们也来看看他在日常开发中的业务场景:

  • 比如我们常见的谷歌搜索框的联想功能
  • 监听浏览器的滚动加载事件
  • 高频鼠标点击事件(也可做防抖)
  • 拖拽动画

好啦,关于防抖和节流我们就讲到这里啦,相信你只要动手写一写,应该很快就能掌握的~

文末

欢迎关注「前端光影」公众号,公众号都是以系统专题模块的形式来展示的,这样看起来就会比较方便,系统,让我们一起持续学习各种前端知识,加油!

文章分类
前端
文章标签