什么是防抖和节流?有哪些实现方式?

2,255 阅读5分钟

通俗理解

防抖

平时我们买手机或者相机,都具有防抖功能,相机的防抖和这里防抖逻辑上是一样的。当我们拿起相机记录视频的时候,我们的手会不停的抖动,但是拍出的视频抖的却不是很厉害,原因就是相机把n毫秒之内的抖动给忽略掉,使相机的镜头始终保持在n毫秒之前的状态。

节流

物体的移动在空间和时间上是连续的,但是我们记录的视频却是一帧一帧的,原因就是相机忽略了n毫秒内的物体移动,以节省存储空间。这个过程可以理解为节流。

其实说明一些问题未必要举和事物本身不一样的东西,那接下来我们就拿开发网页时真实遇见的场景。

真实场景

场景

你正在开发一个用户注册页面,平台里规定,昵称字段必须唯一,但是不要等用户点击注册按钮的时候才去检查,要做到尽快的去检查。

首先我们想到,每当输入框改变的时候,我们就检查一次是否已经有人注册该昵称。如果用户很快速的输入,显然这样查询太快,对服务器压力太大。

面对这个问题,我们可以想到两个方案来解决。

1. 防抖

输入框的前后两次改变超过n毫秒才执行查询,只要改变时间间隔小于n毫秒就不执行查询。

2. 节流

n毫秒内的改变只执行一次查询。

是不是和相机的例子很像?

我们有了两种解决方案,到了代码层面怎么实现呢?我们先试试用直截了当的方式实现一下。

简单实现

1. 防抖:

<input name="username" type="text" oninput="checkUsername(this)">
<script>
    var handler;
    function checkUsername(it) {
        clearTimeout(handler);
        handler = setTimeout(() => {
            // TODO
        }, 1000)
    }
</script>

可以仔细分析一下,这几行代码是不是上面的方案一。

如果在1000毫秒内,反复执行checkUsername函数,那么setTimeout就会被反复清除,并且反复设置一个新的1000毫秒的定时任务。直到你执行checkUsername的前后两次间隔超过1000毫秒,定时任务才有机会执行一次。

2. 节流:

<input name="username" type="text" oninput="checkUsername(this)">
<script>
    var isRuning = false;
    function checkUsername(it) {
        if (!isRuning) {
            isRuning = true;
            // TODO
            setTimeout(() => isRuning = false, 1000)
        }
    }
</script>

可以分析一下这几行代码。

第一次执行checkUsername的时候,!isRuning为ture,执行关键代码,改变isRuning为true,设置一个1000毫秒后再把isRuning再改为false。那么,当你在1000毫秒内反复执行checkUsername的话,关键代码都不会执行,只有等到定时器执行后,再次执行checkUsername,才会执行关键代码。

以上代码都是直接了当的实现了需求。但是防抖和节流的需求很多,仔细分析,我们发现,有些代码属于样板代码,和业务逻辑无关,我们的业务逻辑就在TODO,所以,我们能不能提炼一下呢?

重构

防抖

分离

const debounce = (fun, interval) => {
    let handle;
    return (obj) => {
        clearTimeout(handle);
        handle = setTimeout(() => fun(obj), interval);
    }
}

首先,你写了一个函数,这个函接受两个参数,参数的含义是,一个是待执行的函数(TODO),一个是时间间隔。函数返回一个函数,这个函数如果连续调用的话,它的执行就会符合防抖的定义。可以套用上面的逻辑分析一下。

应用

<input name="username" type="text" oninput="checkUsername(this)">
<script>
    const checkUsername = debounce(x => {
        // TODO
    }, 1000);
</script>

这样,我们只需要关注业务代码就可以了。

节流

分离

const throttle = (fun, wait) => {
    let isRuning = false;
    return (obj) => {
        if (!isRuning) {
            isRuning = true;
            setTimeout(() => isRuning = false, wait)
            fun(obj);
        }
    }
}

应用

<input name="username" type="text" oninput="checkUsername(this)">
<script>
    const checkUsername = throttle(x => {
        // TODO
    }, 1000);
</script>

无甚好说,不复杂。有疑问,可留言。

装饰器模式

或许你的项目引入typescript,正好TS很好的支持装饰器模式,于是你想用装饰器实现该功能。讲真,这个方式更优雅。

防抖

分离

先实现一个防抖的方法装饰器。

export default function debounce (interval: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    let handle:number
    const fun = descriptor.value as Function
    descriptor.value = function (...obj:any) {
      clearTimeout(handle)
      handle = setTimeout(() => fun.call(this, obj), interval)
    }
  }
}

如果不太理解装饰器的话,可以看我前面的文章,或者去网上查找。不过,我的示例,具有实际应用价值。

核心代码和上面的基本一致,这里要套用ts装饰器的语法,及理解this在里面的指向。这个必须理解装饰器相关知识点才能理解。

应用

这里借助Vue项目演示。

<template>
  <input name="username" type="text" @input="checkUsername">
</template>

<script lang='ts'>
import { Vue, Component } from 'vue-property-decorator'

+import debounce from './debounce'

@Component({})
export default class Test extends Vue {
+ @debounce(1000)
  checkUsername () {
    // TODO
  }
}
</script>

应用无甚好说。

节流也一并写出来,无甚好说。有问题,欢迎留言。

节流

抽离

export default function throttle (wait: number) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const fun = descriptor.value as Function
    let isRuning = false
    descriptor.value = function (...obj: any) {
      if (!isRuning) {
        isRuning = true
        setTimeout(() => { isRuning = false }, wait)
        fun.call(this, obj)
      }
    }
  }
}

应用

<template>
  <input name="username" type="text" @input="checkUsername">
</template>

<script lang='ts'>
import { Vue, Component } from 'vue-property-decorator'

+import throttle from './throttle'

@Component({})
export default class Test extends Vue {
+ @throttle(1000)
  checkUsername () {
    // TODO
  }
}
</script>

总结

还有其它方式,比如观察者模式,大概思路是,把不断触发的那个事件转化成自定义事件,然后在自定义事件里应用防抖或者节流把这个事件放缓,最终由观察者处理这个放缓的事件。这个方案暂时就不写在这里了。

其实,我们遇到问题,一旦了解问题的现象及本质,就可以搜寻自己的知识结构,找到对应的解决方案。

注:自己读了下,发现几处语句有问题,调整后重新发布。