Vue3 中的 shallowRef 技巧

前言

使用 Vue3 至今为止算是熟悉,但不能说很熟练了,比如针对 shallowRef, shallowReactive 这类就不算特别熟悉,但当初没有仔细看文档的回旋镖总会砸到自己头上的。

最近开发一个数据大量可视化的项目,引用大量外部类库文件,在更新数据的时候发现网页性能大幅度下降,使用性能定位工具看了下是深度响应式问题导致的:大量使用 ref 定义外部类库函数,导致系统一直索引内部深度响应式。

shallowRef

因为之前开发几乎没有遇到大数据性能问题,直接梭哈 ref ,这次算是遇到坑了,其实解决方法也简单就是将 ref 换成 shallowRef 就行了,所以说 shallowRef 是有什么特性:ref() 的浅层作用形式,说人话就是只有替换最顶层的值才能响应式:

import { shallowRef } from 'vue'

const state = shallowRef({ count: 0 })

// 直接修改嵌套属性不会触发更新
state.value.count = 1 // ❌ 不会触发视图更新

state.value = { count: 1 } // ✅ 这样才能触发响应

所以说这个使用场景在:

  • 用在外部类库声明上,比如 ECharts.js 组件或者说富文本组件等等。
  • 还有就是一些大量数据声明:比如后端给个数据,需要频繁变化,但不想关心内部结构,只需要在意是否有变化。
  • 父组件管理子组件一些数据维护开支等。

进阶学习

思考后感觉没这么简单,实践后发现一些奇淫技巧,以后开发会用到的,就笔记下来了:

手动更新

如果现在有个场景一个数据使用 shallowRef 声明了很多,但发现替换顶层数据太麻烦了,只想更新其中一个对象属性,这时候可以把 triggerRef 拉出来使用了:

import { shallowRef, triggerRef } from 'vue';

const state = shallowRef({ count: 0 });

// 直接修改嵌套属性不会触发更新
state.value.count = 1; // ❌ 不会触发视图更新

// 手动强制触发更新
triggerRef(state); // ✅ 强制触发响应

这一般可以用在数据频繁修改或者大型数据修改的时候,可以避免性能追踪开销,只要手动修改就行。

再比如在动画实时变化的场景下:

import { shallowRef, onMounted } from 'vue';

const data = shallowRef({ x: 0, y: 0 });

onMounted(() => {
  requestAnimationFrame(() => {
    data.value.x++; // 高频修改属性
    if (data.value.x === 10000) triggerRef(data); // 手动触发更新(按需调用)
  });
});

混合响应式数据

之前遇到过一种数据,大概有五六层深度的对象数据,需要变动三层某个属性数据,这时候利用 shallowRefreactive 完成:

import { shallowRef, reactive } from 'vue';

const refState = shallowRef({ 
  config: reactive({ enabled: true }), // 部分属性保持响应式
  metadata: { version: '1.0' }         // 非响应式部分
});

refState.value.config.enabled = false; // ✅ 触发响应
refState.value.metadata.version = '2.0'; // ❌ 不触发响应

customRef 的一些技巧

正常来说能用到 customRef 场景很少,官方文档是不建议使用这个,除非需要对变量的细腻度追踪,Demo 代码建议去文档查看,这边就展示一些常用的代码技巧:

  • 追踪一个变量的变化历史,比如编辑器的用户操作场景:
function historyRef(initialValue) {
  let history = [initialValue];
  return customRef((track, trigger) => ({
    // 读取动作
    get() {
      track();
      return history[history.length - 1];
    },
    // 记录动作
    set(newValue) {
      history.push(newValue);
      trigger();
    },
    // 撤回动作
    undo() {
      if (history.length > 1) {
        history.pop();
        trigger();
      }
    }
  }));
}
  • 数据验证拦截
function validatedRef(initialValue, validator) {
  return customRef((track, trigger) => ({
    get() {
      track();
      return initialValue;
    },
    set(newValue) {
      if (validator(newValue)) {
        initialValue = newValue;
        trigger();
      } else {
        console.warn('Invalid value!');
      }
    }
  }));
}

// 使用
const age = validatedRef(18, (v) => v >= 0);
  • 非响应库集成
import { customRef } from 'vue';
import { NonReactiveStore } from 'some-library';

const store = new NonReactiveStore();

const storeRef = customRef((track, trigger) => ({
  get() {
    track();
    return store.getState();
  },
  set(newValue) {
    store.setState(newValue);
    trigger();
  }
}));

// 监听外部库变化
store.onChange(() => {
  trigger(storeRef); // 外部变化时手动触发更新
});
  • 缓存计算
function cachedRef(initialValue, computeFn) {
  let cache = null;
  return customRef((track, trigger) => ({
    get() {
      track();
      if (cache === null) {
        cache = computeFn(initialValue);
      }
      return cache;
    },
    set(newValue) {
      initialValue = newValue;
      cache = null; // 清空缓存
      trigger();
    }
  }));
}

// 使用
const expensiveData = cachedRef(0, (n) => heavyCalculation(n));