防抖就是对于频繁触发的事件添加一个延时同时设定一个最小触发间隔,如果触发间隔小于设定的间隔,则清除原来的定时,重新设定新的定时;如果触发间隔大于设定间隔,则保留原来的定时,并设置新的定时;
防抖的结果就是频繁的触发转变为触发一次。
接下来,让我们看一个具体的业务需求。
在项目中,我们有一个输入框,我们希望在用户输入后间隔一段时间再执行指定的逻辑。同时,由于防抖需要在项目中多处使用,因此我们希望能够尽可能方便地重复使用,并且代码精简。
思考
我们先来看一个简单的效果:在输入框中输入文本,下方会显示我们输入的文本。
{{ text }}
import { ref } from 'vue';
const text = ref('');
然后我们现在要加入防抖功能,这意味着数据的变化不能立即发生,而是要等待一小段时间后再发生变化。
因此不能再使用 v-model,因为 v-model 的双向绑定是数据及时响应变化的。
不过我们可以利用 v-model 的特性,将其拆分为 :value="" 和 @input="" 的形式。
{{ text }}
import { ref } from 'vue';
const text = ref('');
const handleUpdate = (e) => {
text.value = e.target.value
}
这种形式就是 v-model 的原始形式,效果同 v-model 是一致的。
改成这种写法之后,我们就可以利用 handleUpdate 函数进行操作了。
因为这里的防抖无非就是让数据的变化延迟执行,所以我们可以这样写:
import { ref } from 'vue';
const text = ref('');
// 我们在这里定义一个 timer
let timer;
const handleUpdate = (e) => {
// 每一次执行这个函数的时候我们将 timer 情况
clearTimeout(timer)
// 然后再执行 setTimeout,比如说间隔 1000ms
setTimeout(() => {
text.value = e.target.value
}, 1000)
}
/**
* @description: 防抖函数
* @param {Function} func: 要延迟执行的函数
* @param {Number} delay: 延迟时间 ( 默认 1000ms )
*/
export function debounce(func, delay = 1000) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.call(this, ...args);
}, delay);
};
}
这样我们就封装好了一个防抖函数。
现在让我们在组件中使用它。
import { ref } from 'vue';
import { debounce } from './debounce';
const text = ref('');
const update = (e) => (text.value = e.target.value)
// 将 update 和 延迟时间传入进去
// 当数据变化的时候,运行的是防抖函数
// 那么防抖函数会隔一段时间再去运行这个 update 函数,去更新数据
const handleUpdate = debounce(update, 1000)
现在运行一下,效果还是没问题的。
但是现在看起来还是不够舒服,而且 v-model 原本用得好好地却被换成了 :value="" 和 @input="" 的形式。
而且每次使用时还需要写很多冗余代码,这我可忍受不了。
但是要继续优化就需要考验能力了,我们说过我们要做到极致的防抖!
那当然要在最初的代码模式上做防抖才算最完美。
首先让我们观察一下 const text = ref('') 得到了什么。我们在控制台中输出 text 看看。
通过输出结果可以看出,text 得到了一个叫做 RefImpl 对象。
这个对象里有一个 value 属性,当你看到 value 属性是 (...) 时,你应该立刻明白它是通过Object.defineProperty 定义的,通过定义之后就会产生 get 和 set 两个函数。
也就是说,ref 的源代码看上去就像是 get 和 set 的结构。
根据上面发现,让我们写出 ref 方法的结构:
// ref 方法需要一个值,我们写作 value
function ref(value) {
// 它返回一个对象
{
// 对象中有一个 value 属性
// 是通过 Object.defineProperty 定义的
// 定义里会有一个 get 和 set 方法
get(){
// get 方法返回 value
return value
},
set(val){
// set 方法给 value重新赋值
value = val
}
}
}
// 大概就是这个样子
// 但是不要忘记了 vue 是带有响应式的
// 而响应式的原理是什么呢?
// 就是在 get 的时候进行依赖收集,就是说谁用到了我这个数据
// 而 set 的时候叫做派发更新,表示通知使用数据的地方数据更新了
// 那么就应该是如下的样子
function ref(value) {
{
get(){
// 依赖收集,我们称为 track()
return value
},
set(val){
// 派发更新 trigger()
value = val
}
}
}
那么这跟我们要做的防抖有什么关系呢?
再看一下:如果能够延迟 set 的派发更新,也就是说将 set 放到一个防抖函数中延迟执行,那么问题就解决了。
那是否意味着我们要重新写一个 ref 呢?将其源代码重新实现一遍?其实没有必要。
因为 vue 提供了一个自定义 ref 入口:customRef。
通过 customRef 可以自定义 get 和 set 方法。
那么通过 customRef 我们就看到了胜利的曙光了,让我们利用它写出自己的 ref。
实现
import { customRef } from "vue";
/**
* @description: 防抖的 ref
* @param {*} value: ref 的 value
* @param {*} delay: 延迟时间,表示 value 的更新要在多少时间后触发 ( 默认为 1000ms )
*/
export function debounceRef(value, delay = 1000) {
let timer;
// 我们这里利用 customRef 来自定义 ref
return customRef(() => {
// customRef 中返回一个对象,对象中包含 get 和 set 函数
return {
get() {
// 依赖收集 track()
return value;
},
set(val) {
// 使用 setTimeout 实现防抖
clearTimeout(timer);
timer = setTimeout(() => {
// 派发更新 trigger()
value = val;
}, delay);
},
};
});
}
上面代码中我们实现了自定义 ref,并实现了防抖功能。
但缺少最关键依赖收集和派发更新部分,我们没有时间重新实现依赖收集源码和派发更新源码。
但是 vue 贴心的考虑到了这一点,它给我们传入了两个参数,track 和 trigger,分别表示依赖收集和派发更新,我们只要在合适的时候调用即可。
import { customRef } from "vue";
export function debounceRef(value, delay = 1000) {
let timer;
return customRef((track, trigger) => {
// 获得 track, trigger 函数
return {
get() {
// 依赖收集 track()
track();
return value;
},
set(val) {
clearTimeout(timer);
timer = setTimeout(() => {
value = val;
// 派发更新 trigger()
trigger();
}, delay);
},
};
});
}
现在去使用一下,看看效果。
{{ text }}
// 在使用时,我们要引入自定义的 ref
import { debounceRef } from './debounceRef';
const text = debounceRef('');
我们输出 text 到控制台看看。
可以看出得到的是一个 CustomRefImpl 对象,这个 value 呢,也是一个 get 和 set,只不过现在用的是我们自己写的 get 和 set。
来试一下效果。
领取专属 10元无门槛券
私享最新 技术干货