Vue3中极致的数据防抖-customRef

1,318 阅读3分钟

前言

今天在看vue3的文档时,发现了下面这个钩子函数,在看到官方给出的示例后,顿时觉得惊为天人,于是迫不及待的自己上手试了试,并在此分享出来。(说明官方给的文档真是要琢磨透啊,很多在别的地方看到的骚操作,其实都是官方给出来的🤣)

image.png

首先从文档中我们可以看到官方对这个钩子的定义的是可以创建一个自定义的ref,显示声明对其依赖追踪和更新触发的控制方式。 其实单看这个解释,我们并不知道这个钩子到底可以干什么用,下面我们将从数据防抖的实现中去看这个函数的具体使用。

数据防抖

相信做前端的同学们对防抖肯定都不陌生,简单来说就是控制用户某种触发动作的频率,以缓解请求压力或者避免重复动作。

以如下的例子来说:

9adem-28nff.gif

当我们在输入框输入数据的时候,监听了数据的变化,通常我们是当数据改变时,需要重新请求服务端(比如拿到匹配的选项)。但用户可能会连续输入,导致大量的请求,会造成请求压力,以及获取的匹配信息也会更新的过于频繁。

这是当前的代码:

<template>
  <div class="hello">
    <p>数据防抖</p>
    <input type="text" v-model="inputValue" />
    <p>{{ inputValue }}</p>
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
    const inputValue = ref('');
    return {
      inputValue,
    };
  },
};
</script>

可以看到我们在input中使用了v-model双向绑定了响应式数据变量inputValue,其实在这个时候我们已经可以使用类似watch的方式来监听数据。但由于频繁的输入,会使监听一直触发。所以我们来稍作修改:

<template>
  <div class="hello">
    <p>数据防抖</p>
    <input type="text" :value="inputValue" @input="inputChange" />
    <p>{{ inputValue }}</p>
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  name: 'HelloWorld',
  setup() {
    let inputValue = ref('');
    let timer = null;
    const inputChange = (e) => {
      clearTimeout(timer);
      timer = setTimeout(() => {  // 延迟更新inputValue
        inputValue.value = e.target.value;
      }, 500);
    };
    return {
      inputValue,
      inputChange,
    };
  },
};
</script>

这时可以看到,其实我是做了一个十分简单的防抖操作,即不使用v-model绑定,而是改为手动更新inputValue,并且延时了500毫秒。那么看在代码,大家其实都知道,这时候即便用户频繁的输入,也会间隔一定时间后再进行数据交互。但明显,这样很不优雅,并且不容易复用,显得我们很睿智。

1675322392034.png

所以就成了下面这种普遍的做法:

<template>
  <div class="hello">
    <p>数据防抖</p>
    <input type="text" :value="inputValue" @input="inputChange" />
    <p>{{ inputValue }}</p>
  </div>
</template>

<script>
import { ref } from 'vue';
import { debounce } from './debounce';
export default {
  name: 'HelloWorld',
  setup() {
    let inputValue = ref('');
    const updateFunc = (e) => {
      inputValue.value = e.target.value;
    };
    const inputChange = debounce(updateFunc, 1000);
    return {
      inputValue,
      inputChange,
    };
  },
};
</script>
// debounce.js
export function debounce(func, delay = 500) {
  let timer;

  return function (...args) {
    clearTimeout(timer);
    setTimeout(() => {
      func.call(this, ...args);
    }, delay);
  };
}

这里我们在debounce.js中封装了一个通用的防抖函数,传入回调函数和延迟执行时间。其实到这里,就是我们传统意义上的防抖做法了,可能有的人是再继续优化防抖函数内的代码,或者直接使用类似lodash中的防抖。

但是这样写,每次我们都需要再单独声明更新函数,也就是上述的updateFunc。那么下面我们看看怎么利用customRef吧。

customRef

我们再看看下文档对customRef的详细说明:

image.png

看着是不是与我们以前使用的Object.defineProperty很像?其实就是一个数据劫持,所以我们可以声明一个自己的用于防抖的ref函数:

// debounceRef.js
import { customRef } from 'vue';

export function useDebounceRef(value, delay = 200) {
  let timer;
  return customRef((track, trigger) => {
    return {
      get() {
        // 所有依赖收集 track
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timer);
        timer = setTimeout(() => {
          value = newValue;
          // 更新触发 trigger
          trigger();
        }, delay);
      },
    };
  });
}

export default useDebounceRef;

然后在我们的页面中这样使用:

// app.vue
<template>
  <div id="app">
    <input type="text" v-model="inputValue" />
    <p>
      {{ inputValue }}
    </p>
    <HelloWorld></HelloWorld>
  </div>
</template>

<script>
import { ref } from 'vue';
import { useDebounceRef } from './debounceRef';
export default {
  name: 'App',
  setup() {
    const inputValue = useDebounceRef('', 1000);
    return {
      inputValue,
    };
  },
};
</script>

如此一来,我们即利用了原有的双向绑定,又实现了数据防抖,同时在使用时仅需要一行代码。

个人感觉是如此的优雅,分享给大家。在线demo

image.png