如何利用 customRef 在 vue3 中将防抖做到极致

防抖你一定学过,但是你知道如何在 Vue3 里中将防抖做到极致吗?什么样的防抖才算是极致呢?

大家好,我是渡一前端子辰老师,首先,让我们回顾一下防抖的概念。

防抖就是对于频繁触发的事件添加一个延时同时设定一个最小触发间隔,如果触发间隔小于设定的间隔,则清除原来的定时,重新设定新的定时;如果触发间隔大于设定间隔,则保留原来的定时,并设置新的定时;

防抖的结果就是频繁的触发转变为触发一次。

接下来,让我们看一个具体的业务需求。

在项目中,我们有一个输入框,我们希望在用户输入后间隔一段时间再执行指定的逻辑。同时,由于防抖需要在项目中多处使用,因此我们希望能够尽可能方便地重复使用,并且代码精简。

你可以在这里停一下,自己去尝试尝试,子辰在这里等你一会。

思考

我们先来看一个简单的效果:在输入框中输入文本,下方会显示我们输入的文本。

html 复制代码
<template>
  <div class="container">
    <input v-model="text" />
    <p class="result">{{ text }}</p>
  </div>
</template>
<script setup>
import { ref } from 'vue';
const text = ref('');
</script>

然后我们现在要加入防抖功能,这意味着数据的变化不能立即发生,而是要等待一小段时间后再发生变化。

因此不能再使用 v-model,因为 v-model 的双向绑定是数据及时响应变化的。

不过我们可以利用 v-model 的特性,将其拆分为 :value=""@input="" 的形式。

html 复制代码
<template>
  <div class="container">
    <!-- @input 绑定一个 handleUpdate 函数 -->
    <input :value="text" @input="handleUpdate" />
    <p class="result">{{ text }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue';
const text = ref('');
const handleUpdate = (e) => {
  text.value = e.target.value
}
</script>

这种形式就是 v-model 的原始形式,效果同 v-model 是一致的。

改成这种写法之后,我们就可以利用 handleUpdate 函数进行操作了。

因为这里的防抖无非就是让数据的变化延迟执行,所以我们可以这样写:

html 复制代码
<script setup>
  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)
  }
</script>
js 复制代码
/**
 * @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);
  };
}

这样我们就封装好了一个防抖函数。

现在让我们在组件中使用它。

html 复制代码
<script setup>
  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)
</script>

现在运行一下,效果还是没问题的。

但是现在看起来还是不够舒服,而且 v-model 原本用得好好地却被换成了 :value=""@input="" 的形式。

而且每次使用时还需要写很多冗余代码,这我可忍受不了。

但是要继续优化就需要考验能力了,我们说过我们要做到极致的防抖!

那当然要在最初的代码模式上做防抖才算最完美。

首先让我们观察一下 const text = ref('') 得到了什么。我们在控制台中输出 text 看看。

通过输出结果可以看出,text 得到了一个叫做 RefImpl 对象。

这个对象里有一个 value 属性,当你看到 value 属性是 (...) 时,你应该立刻明白它是通过Object.defineProperty 定义的,通过定义之后就会产生 get 和 set 两个函数。

也就是说,ref 的源代码看上去就像是 get 和 set 的结构。

根据上面发现,让我们写出 ref 方法的结构:

js 复制代码
// 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。

实现

js 复制代码
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 贴心的考虑到了这一点,它给我们传入了两个参数,tracktrigger,分别表示依赖收集和派发更新,我们只要在合适的时候调用即可。

js 复制代码
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);
      },
    };
  });
}

现在去使用一下,看看效果。

html 复制代码
<template>
  <div class="container">
    <input v-model="text" />
    <p class="result">{{ text }}</p>
  </div>
</template>

<script setup>
// 在使用时,我们要引入自定义的 ref
import { debounceRef } from './debounceRef';
const text = debounceRef('');
</script>

我们输出 text 到控制台看看。

可以看出得到的是一个 CustomRefImpl 对象,这个 value 呢,也是一个 get 和 set,只不过现在用的是我们自己写的 get 和 set。

来试一下效果。

总结

通过现在的代码,我们可以轻松实现防抖的功能,而且使用时非常简单,只需要一个 ref 就可以做到。

这种方式给我们带来了很大的便利,让我们在开发过程中更加高效。

感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!

本文来源

本文来源自渡一官方公众号:Duing ,欢迎关注,获取最新最全最深入的技术讲解

感谢你阅读本文,如果你有任何疑问或建议,请在评论区留言,如果你觉得这篇文章有用,请点赞收藏或分享给你的朋友!

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax