写Vue大篇幅的ref、computed,而reactive为何少见?
在使用Vue3开发项目时,经常会遇到一个vue文件中定义了不下20个ref和computed,这不仅让代码显得冗长,也使得开发者感到困惑。那么有没有更好的解决方案呢?Vue设计ref、computed、reactive的初衷又是什么?让我们从源码和实践中找答案!
理解定义的初衷
Vue官方文档在介绍响应式对象的定义时,顺序为ref、computed、reactive、readonly,这与项目开发中它们的使用频率大致相符。
ref
我们先抛出一个问题:如果给ref b传入的参数a本身也是一个ref类型的值,是通过b.value还是b.value.value来获取值?
const a = ref(1);
const b= ref(a);
// console.log(b.value);
// console.log(a.value);
ref的定义:
function ref<T>(value: T): Ref<UnwrapRef<T>> {
interface Ref<T> { value: T }
}
传入的value可以是简单值或复杂对象,返回的类型是Ref<UnwrapRef<T>>
,确定结果是{ value: x }
格式对象。
export type UnwrapRef<T> =
T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
通过上述代码分析,ref
函数需要对原始值进行处理:如果rawValue是Ref类型,则直接返回rawValue,不做处理。因此,代码中的a === b
。
const a = ref({ x: 1, y: 2 });
const b= ref(a);
如果rawValue是非Ref类型,则返回RefImpl
的实例化对象。
reactive
reactive
方法用于返回对象的深层响应式代理,适用于对象类型,默认会解包ref类型。
const count = ref(1);
const obj = reactive({ count });
console.log(obj.count === count.value); // true
需要注意,如果reactive包含集合对象,集合本身不会被解包。
const books = reactive([ref('Vue 3 Guide')]);
console.log(books[0].value);
computed
computed
接受一个getter函数,返回一个只读的响应式ref对象。其定义如下:
function computed<T>(
getter: (oldValue: T | undefined) => T,
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
也可以定义可读可写的computed
:
function computed<T>(
options: {
get: (oldValue: T | undefined) => T;
set: (value: T) => void;
},
debuggerOptions?: DebuggerOptions
): Ref<T>
示例:
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1;
},
});
在这个例子中,plusOne
允许我们通过set
方法修改count
的值,而这个特性在某些场景下非常有用。
watch监听Ref类型值的策略
watch
函数可以监听Ref类型的值,不同的监听策略如下:
watch(refData, ...)
可以触发回调函数,但不会监听深层属性的变化;watch(refData.value, ...)
可以监听深层属性的变化,但不能监听refData.value
本身的变化。
深入研究源码可以发现,watch
函数通过getter
来取值,当source是Ref
类型时,直接返回source.value
。
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => reactiveGetter(source);
}
什么时候使用computed
computed
的优势在于,当getter函数中的响应式数据发生变化时,它会自动更新结果。
const name = ref('李磊');
const detail = reactive({ phone: 10000 });
const cptValue = computed(() => {
return `${name.value}: ${detail.phone}`;
});
watch(cptValue, (newValue) => {
console.log(newValue);
});
如何简化组件的复杂度
在Vue组件中,当功能复杂度增加时,定义的ref和computed会迅速增加。为了简化,我们可以将逻辑封装到外部模块,例如使用dialog.ts
模块集中管理逻辑:
export const useDialog = () => {
const title = ref('');
const visible = ref(false);
// 其他逻辑...
return {
title,
visible,
};
};
这样,在Vue文件中可以直接引用逻辑模块:
const { title, visible } = useDialog();
总结
- ref适用于简单值的定义,且可以作为响应式引用的基础。
- computed用于需要依赖其他响应式数据进行动态计算的场景。
- reactive适用于对象类型的深度响应式需求,但在项目中使用较少。
通过将复杂的逻辑分离到独立的模块中,不仅能减少代码重复,还能提升项目的可维护性。