笔者曾看到一些项目为了使用一个防抖函数而引入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是保证一定时间内多次请求至少有一次调用。本文当中有理解不对的地方请留言纠正,谢谢!