可选反应性#
import { Ref, ref } from 'vue';
type MaybeRef<T> = Ref<T> | T;
// 示例用法
// ✅ 有效
const raw: MaybeRef<number> = 1;
// ✅ 有效
const reffed: MaybeRef<number> = ref(1);
isRef(raw); // false
isRef(reffed); // true
unref(raw); // 1
unref(reffed); // 1
示例:
import { unref, ref, isRef, onMounted, watch } from 'vue';
import { MaybeRef } from '@/types';
async function fetchProduct(id: number) {
// 从 API 返回产品
return fetch(`/api/products/${id}`).then((res) => res.json());
}
export function useProduct(id: MaybeRef<number>) {
const product = ref(null);
onMounted(async () => {
product.value = await fetchProduct(unref(id));
});
if (isRef(id)) {
watch(id, async (newId) => {
product.value = await fetchProduct(newId);
});
}
return product;
}
import { useProduct } from '@/composables/products';
import { computed } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
// 仅在挂载时获取一次
const product = useProduct(route.params.productId);
// 每当参数变化时保持同步
const product = useProduct(
computed(() => route.params.productId)
);
避免重新打包 refs#
import { reactive, watch } from 'vue';
const obj = reactive({
id: 1,
});
// ✅ 有效!
watch(obj, () => {
// ...
});
// ❌ 无效
watch(obj.id, () => {
// ...
});
最后一个 watch
不太有效,因为当你访问 props.id
时,你得到的是一个非反应性的版本,这是原始的 prop 值。因此,为了保持其反应性,你可能需要使用一些技巧:
import { reactive, watch, toRef, toRefs, computed } from 'vue';
const obj = reactive({
id: 1,
});
// 将所有条目转换为 refs
const objRef = toRefs(obj);
watch(objRef.id, () => {
//...
});
// 你也可以解构它
const { id: idRef } = toRefs(obj);
watch(idRef, () => {
//...
});
// 将单个条目转换为其反应性版本
const idRef = toRef(obj, 'id');
watch(idRef, () => {
//...
});
// 仅在计算属性中提取值
const idComp = computed(() => obj.id);
watch(idComp, () => {
//...
});
上述所有语句都从反应性对象值或任何对象 ref 中产生一个反应性的 id
值。然而,这会造成一个问题。用户必须在将其传递给函数之前适当地构造反应性数据,这迫使他们使用任何可用的打包机制。
根本问题在于,这个过程迫使消费者解包他们的值,并在希望保持反应性时重新打包它们。这种做法往往会导致冗长,我喜欢称之为 “重新打包 refs”。
解决方案#
import { Ref } from 'vue';
// 原始值或 ref
export type MaybeRef<T> = Ref<T> | T;
// 不能是原始值
export type LazyOrRef<T> = Ref<T> | (() => T);
// 可以是 ref、getter 或原始值
export type MaybeLazyRef<T> = MaybeRef<T> | (() => T);
import { unref } from 'vue';
export function unravel<T>(value: MaybeLazyRef<T>): T {
if (typeof value === 'function') {
return value();
}
return unref(value);
}
export function isWatchable<T>(
value: MaybeLazyRef<T>
): value is LazyOrRef<T> {
return isRef(value) || typeof value === 'function';
}
示例:
import { ref, onMounted, watch } from 'vue';
import { unravel, isWatchable } from '@/utils';
import { MaybeLazyRef } from '@/types';
async function fetchProduct(id: number) {
// 从 API 返回产品
return fetch(`/api/products/${id}`).then((res) => res.json());
}
export function useProduct(id: MaybeLazyRef<number>) {
const product = ref(null);
onMounted(async () => {
product.value = await fetchProduct(unravel(id));
});
if (isWatchable(id)) {
// 有效,因为 getter 函数或 ref 都是可观察的
watch(id, async (newId) => {
product.value = await fetchProduct(newId);
});
}
return product;
}
import { useRoute } from 'vue-router';
import { useProduct } from '@/composables/products';
const route = useRoute();
// 仅在挂载时获取一次
const product = useProduct(route.params.productId);
// 每当参数变化时保持同步
const product = useProduct(() => route.params.productId);
需要反应性#
远离我们之前的示例,假设你正在尝试构建一个仅接受反应性位置参数的 usePositionFollower
函数,表示为 x,y 坐标。获取一个非反应性的位置是没有意义的,因为没有任何东西可以跟随。
因此,告知用户仅提供反应性值,或更具体地说,反应性表达式将是有益的。这排除了使用 MaybeLazyRef
,但请记住,我们之前创建了 LazyOrRef
,可以与 isWatchable
函数一起使用。
import { h, defineComponent } from 'vue';
import { LazyOrRef } from '@/types';
import { unravel } from '@/utils';
export function usePositionFollower(
position: LazyOrRef<{ x: number; y: number }>
) {
const style = computed(() => {
const { x, y } = unravel(position);
return {
position: 'fixed',
top: 0,
left: 0,
transform: `translate3d(${x}px, ${y}px, 0)`,
};
});
const Follower = defineComponent(
(props, { slots }) =>
() =>
h('div', { ...props, style: style.value }, slots)
);
return Follower;
}
const { x, y } = useMouse();
const Follower = usePositionFollower(() => ({
x: x.value,
y: y.value,
}));