Skip to content

在 Vue 3 的响应式系统中,customRef 是一个功能强大但常被忽视的 API。它允许开发者完全自定义一个 ref 的响应式行为,直接暴露底层的 tracktrigger 机制。这种低级别的控制能力,使得实现复杂的响应式逻辑成为可能。本节将通过手写一个“防抖 ref”的实例,深入剖析 customRef 的设计原理,并揭示其在构建高级响应式工具中的核心价值。

一、customRef 的 API 设计

customRef 接收一个工厂函数作为参数,该函数返回一个包含 getset 方法的对象:

ts
function customRef(factory) {
  const ref = factory(
    () => track(ref, 'value'),   // track 函数
    () => trigger(ref, 'value')  // trigger 函数
  );
  ref.__v_isRef = true;
  return ref;
}

factory 函数接收两个参数:tracktrigger。开发者可以在 get 中调用 track 来声明依赖,在 set 中调用 trigger 来通知依赖更新。customRef 不提供自动的响应式追踪,一切行为由你定义

这与 ref 形成鲜明对比:

  • ref:自动在 getter 中 track,在 setter 中 trigger
  • customRef:完全手动控制何时 tracktrigger

二、需求分析:为什么需要防抖 ref?

在用户交互场景中,频繁的状态更新(如输入框输入、鼠标移动)会触发大量视图更新,可能导致性能问题。

js
const input = ref('');
watch(input, () => {
  // 每次输入都触发,可能调用 API
});

传统的防抖方案通常在事件处理函数中实现:

js
const debouncedSearch = debounce(() => {
  // 执行搜索
}, 300);

// 在 input 事件中调用 debouncedSearch

但这种方式将防抖逻辑与事件处理耦合,且无法在 watch 或计算属性中直接使用。

一个“防抖 ref”可以将防抖逻辑内置于响应式系统中,使得任何对它的读写都自动具备防抖特性,实现关注点分离。

三、实现:手写 debounceRef

我们将创建一个 debounceRef 函数,它返回一个 customRef,在设置值时进行防抖。

js
import { customRef } from 'vue';

function debounceRef(value, delay = 200) {
  let timeout;

  return customRef((track, trigger) => {
    return {
      get() {
        // 读取时进行依赖收集
        track();
        return value;
      },
      set(newValue) {
        // 清除之前的定时器
        clearTimeout(timeout);
        // 设置新的定时器
        timeout = setTimeout(() => {
          value = newValue;
          // 值更新后,触发依赖更新
          trigger();
        }, delay);
      }
    };
  });
}

实现解析:

  1. 闭包状态

    • value:存储当前值,通过闭包持久化。
    • timeout:存储 setTimeout 的句柄,用于清除定时器。
  2. get 方法

    • 调用 track(),通知 Vue 当前副作用依赖于这个 ref
    • 返回 valuetrack 的调用是响应式读取的基础。
  3. set 方法

    • 接收 newValue
    • 清除之前的 timeout,防止过早触发。
    • 创建新的定时器,在 delay 毫秒后执行:
      • 更新 value
      • 调用 trigger(),通知所有依赖此 ref 的副作用重新执行。

四、使用示例

vue
<template>
  <div>
    <input v-model="text" placeholder="输入搜索关键词..." />
    <p>实时值: {{ text }}</p>
    <p>防抖值: {{ debouncedText }}</p>
  </div>
</template>

<script>
import { ref, computed } from 'vue';
import { debounceRef } from './utils';

export default {
  setup() {
    const text = ref('');
    // 创建防抖 ref,延迟 500ms
    const debouncedText = debounceRef('', 500);

    // 监听防抖后的值
    watch(debouncedText, (val) => {
      if (val) {
        console.log('执行搜索:', val);
        // 调用搜索 API
      }
    });

    // 也可以在 computed 中使用
    const displayText = computed(() => 
      debouncedText.value ? `搜索 "${debouncedText.value}"` : '输入中...'
    );

    return { text, debouncedText, displayText };
  }
}
</script>

在这个例子中:

  • text 是普通 ref,实时响应输入。
  • debouncedText 是防抖 ref,其值在用户停止输入 500ms 后才更新。
  • watchcomputed 依赖于 debouncedText,因此只在防抖后触发。

五、customRef 的高级应用

customRef 的能力远不止防抖。它可以实现各种自定义响应式逻辑:

1. 深度防抖对象

js
function deepDebounceRef(obj, delay) {
  let timeout;
  return customRef((track, trigger) => {
    // 对整个对象进行防抖
    return {
      get() {
        track();
        return obj;
      },
      set(newObj) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          // 深度合并或替换
          Object.assign(obj, newObj);
          trigger();
        }, delay);
      }
    };
  });
}

2. 带脏检查的 ref

js
function dirtyCheckRef(value) {
  let old = value;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        if (newValue !== old) {
          value = newValue;
          old = newValue;
          trigger();
        }
      }
    };
  });
}

仅当新值与旧值不同时才触发更新,避免无意义的渲染。

3. 与外部存储同步

js
function localStorageRef(key, initialValue) {
  const saved = localStorage.getItem(key);
  let value = saved ? JSON.parse(saved) : initialValue;

  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        value = newValue;
        localStorage.setItem(key, JSON.stringify(newValue));
        trigger();
      }
    };
  });
}

自动将 ref 同步到 localStorage

六、设计哲学:暴露底层,赋能开发者

customRef 的设计体现了 Vue 3 响应式系统的一个重要理念:提供底层原语,让开发者构建高级抽象

tracktrigger 是响应式系统的“汇编语言”。refreactive 等 API 都是基于它们的高级封装。customRef 将这些原语直接暴露,使得开发者可以突破默认响应式模型的限制,实现:

  • 精确的更新控制:决定何时以及是否触发更新。
  • 外部系统集成:将 Vue 响应式与 DOM、Storage、WebSocket 等外部状态桥接。
  • 性能优化:避免不必要的依赖追踪和触发。

七、总结

customRef 是 Vue 3 响应式工具箱中的“瑞士军刀”。通过 debounceRef 的实现,我们看到了如何利用 tracktrigger 构建自定义的响应式行为。

关键要点:

  • customRef 完全手动控制 tracktrigger
  • 防抖 ref 通过延迟 trigger 调用来实现更新节流。
  • 闭包用于存储私有状态(如 valuetimeout)。
  • customRef 适用于需要精细控制响应式逻辑的高级场景。

掌握 customRef,意味着你不再局限于 Vue 提供的响应式模式,而是可以按需创造新的响应式原语,这是构建复杂、高性能 Vue 应用的进阶技能。