有了useDebounceFn我可以不用为了一个防抖而引入lodash了

2,825 阅读4分钟

笔者曾看到一些项目为了使用一个防抖函数而引入lodash,笔者的拙见是:我们暂且不考虑tree shaking的作用,整个项目就用lodash的一个防抖而引入lodash那还不如自己照着lodash写一个呢。您怎么看呢?可以留言讨论~

vueuse提供了很多函数,方便我们更好地使用vue3开发项目,其中的useDebounceFn用于对函数执行进行防抖。

其官方文档为了对防抖进行解释打了一个比方:防抖一个超负荷的服务员: 如果你一直请求他,你的请求将被忽略,直到你停下来,给他一些时间考虑你最近的请求。是不是非常形象呢?

本文主要分析useDebounceFn的源码,关于防抖的基本概念本文不再做科普介绍,您可以自行学习其用处以及实现方法,您也可以阅读笔者参与【若川视野源码共读】的文章 跟着underscore学防抖 来进行学习,文章开头推荐了几篇讲解得不错的文章。

1.useDebounceFn使用示例

官方文档demo对应的示例代码如下:

<script setup lang="ts">
import { ref } from 'vue'
import { useDebounceFn } from '@vueuse/core'

const updated = ref(0)
const clicked = ref(0)
const debouncedFn = useDebounceFn(() => {
  updated.value += 1
}, 1000, { maxWait: 5000 })

const clickedFn = () => {
  clicked.value += 1
  debouncedFn()
}
</script>

<template>
  <button @click="clickedFn">
    Smash me!
  </button>
  <note>Delay is set to 1000ms and maxWait is set to 5000ms for this demo.</note>

  <p>Button clicked: {{ clicked }}</p>
  <p>Event handler called: {{ updated }}</p>
</template>

clicked为按钮点击次数,updated为回调函数实际调用次数。代码运行结果如下:

以笔者的点击速度,当连续点击7次,只触发了1次目标函数调用。

2.useDebounceFn源码解析

2.1 useDebounceFn高阶函数

import type { DebounceFilterOptions, FunctionArgs, MaybeRef } from '../utils'
import { createFilterWrapper, debounceFilter } from '../utils'
export function useDebounceFn<T extends FunctionArgs>(fn: T, ms: MaybeRef<number> = 200, options: DebounceFilterOptions = {}): T {
  return createFilterWrapper(
    debounceFilter(ms, options),
    fn,
  )
}

fn是防抖所规定的毫秒数之后要执的回调;ms 是延迟的毫秒数;options是可选的其他选项。useDebounceFn内部调用createFilterWrapper,而createFilterWrapper的一个参数是debounceFilter的调用结果,第二个参数是目标函数fn。

2.2 createFilterWrapper高阶函数

export function createFilterWrapper<T extends FunctionArgs>(filter: EventFilter, fn: T) {
  function wrapper(this: any, ...args: any[]) {
    filter(() => fn.apply(this, args), { fn, thisArg: this, args })
  }

  return wrapper as any as T
}

返回一个函数wrapper, wrapper函数的是对filter的调用。

2.3 debounceFilter:核心角色

一张图,预览一下debounceFilter方法:

声明了两个定时器分别为timer和maxTimer,使用这两个定时器相互结合实现防抖的功能,真的是很美妙啊。说句实在的,笔者水平挺菜的,第一天看没看懂,第二天又看一遍才看懂的。下面我们来分析其具体实现逻辑, 在开始之前为了表述方便先规范一下术语:

  • 回调函数:指setTimeout的回调函数
  • 目标函数:指真正要触发的函数,也就是传给useDebounceFn的第一个参数

2.3.1 清除timer:保证一定时间内多次只调用一次

if (timer)
      clearTimeout(timer)

这里清除timer的目的是为了处理当用户连续调用函数时,为了保证多次之中只触发一次。因为下面的timer回调函数体中会触发目标函数(invoke)的调用。比如用户连续多次点击按钮,那么每一次创建的timer的回调函数还没来得执行,就被下一次点击给清除了。

2.3.2 立即调用

if (duration <= 0 || (maxDuration !== undefined && maxDuration <= 0)) {
  if (maxTimer) {
    clearTimeout(maxTimer)
    maxTimer = null
  }
  return invoke()
}

这段代码是处理duration小于等于0或者maxDuration小于等于0的情况,这两个量小于0代表不要延迟,也就是立即调用的情况。

2.3.3 maxTimer:保证一定时间内多次至少有一次调用

// Create the maxTimer. Clears the regular timer on invokation
if (maxDuration && !maxTimer) {
  maxTimer = setTimeout(() => {
    if (timer)
      clearTimeout(timer)
    maxTimer = null
    invoke()
  }, maxDuration)
}

如果maxDuration存在但是没有创建maxTimer则创建maxTimer,在maxTimer的回调函数中会调用invoke目标函数。maxTimer定时器的意义在于如果用户想多次调用目标函数(invoke)时,它能够保证在maxDuration时间达到的时候能够触发一次。

2.3.4 timer:一段延时后调用目标函数

// Create the regular timer. Clears the max timer on invokation
timer = setTimeout(() => {
  if (maxTimer)
    clearTimeout(maxTimer)
  maxTimer = null
  invoke()
}, duration)

如上所述,timer定时器的作用是为了过了duration毫秒之后调用目标函数。

2.4 调试代码

我们可以将maxWait的值设为2000连续点击:

为了更加直观的理解代码,建议手动调试一下,由于useDebounceFn在实现上使用了高阶函数,函数嵌套一层又一层,可能不太好理解,但是核心逻辑就在debounceFilter这里。笔者把代码简单整理了一下,关键地方增加了log上传至github上了,您可以clone我的代码,调试一下。

3.总结

本文分析了useDebounceFn的源码实现,我们需要清楚timer和maxTimer这两个定时器的作用。timer的作用是保证一段延时间后调用目标函数,maxTimer是保证一定时间内多次请求至少有一次调用。本文当中有理解不对的地方请留言纠正,谢谢!