- 性能调优:
shallowRef
与triggerRef
的精妙配合 - 架构模式: 用
readonly
打造“防弹”的 Composables - 类型体操: 用
generic
让你的组件“活”起来 - TS魔法:
as const
的妙用,告别魔法字符串
Trick 1: 性能调优器 - shallowRef
& triggerRef
场景痛点:
我们都知道 ref
是深度响应的。但如果一个 ref
包裹了层级很深或非常大的对象(比如一个包含数千条数据的列表),每次不经意的改动都可能触发深层的依赖追踪和更新,造成不必要的性能开销。
解决方案: shallowRef
shallowRef
只追踪 .value
属性的变更,不对其内部的属性做响应式处理。
代码示例:
import { shallowRef, triggerRef } from 'vue'
// 假设这是一个非常庞大的对象数组
const bigList = shallowRef([
{ id: 1, name: 'Item 1', data: { /* ... */ } },
// ... 9999 more items
]);
function updateFirstName() {
// 这样做,视图是不会更新的!
// 因为 shallowRef 只关心 .value 本身的替换,不关心内部属性的修改
bigList.value[0].name = 'Updated Item 1';
console.log('Updated name, but no re-render:', bigList.value[0].name);
// 如果想在修改内部数据后,强制触发更新,怎么办?
// 这就是 triggerRef 的用武之地!
triggerRef(bigList);
console.log('Triggered update manually!');
}
核心思想:
用 shallowRef
接管大型数据结构,获得性能提升。当确实需要更新视图时,通过 triggerRef
进行一次“手动通知”。这是一种“我比Vue更懂何时更新”的精细化性能控制。
Trick 2: 架构防弹衣 - readonly
Composables
场景痛点:
我们封装 Composable (自定义Hook) 是为了逻辑复用和封装。但如果一个 Composable 返回了响应式状态(如 ref
),任何使用它的组件都可以直接修改这个状态(例如 useCounter().count.value++
),这破坏了封装性,使得状态变更的来源变得不可追溯,是潜在的BUG温床。
解决方案: 在 Composable 内部,返回状态的只读版本,并暴露专门的方法来修改它。
代码示例:
我们来改造一个经典的 useCounter
。
// useCounter.ts
import { ref, readonly } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
return {
// 将 count 包裹在 readonly() 中再暴露出去
count: readonly(count),
increment,
decrement,
};
}
// MyComponent.vue
import { useCounter } from './useCounter';
const { count, increment } = useCounter();
// count.value++; // ❌ 这行代码会报错!并给出一个友好的警告
// Uncaught TypeError: Set operation on key "value" failed: target is readonly.
// 只能通过暴露的方法来修改状态,保证了数据流的单向和可预测性
increment();
核心思想: 这是一种软件设计模式的体现:“命令查询分离”。Composable 对外只暴露可读的状态和修改状态的“命令”(方法),防止外部直接“篡改”状态,让代码的维护性和健壮性大大提升。
Trick 3: 类型体操 - generic
泛型组件
场景痛点:
我们经常需要创建一些高度可复用的基础组件,比如一个下拉选择器 <Select>
。我们希望它既能接收用户列表 [{id: number, name: string}]
,也能接收产品列表 [{sku: string, title: string}]
,并且在选中时,v-model
和 @change
事件都能返回正确的、带有完整类型信息的对象,而不是 any
或 object
。
解决方案:
在 <script setup>
中使用 generic
属性来定义泛型。 (Vue 3.3+ 支持)
代码示例:
<!-- GenericSelect.vue -->
<script setup lang="ts" generic="T extends { id: any }">
// generic="T" 定义了一个泛型 T
// T extends { id: any } 是一个类型约束,要求传入的对象至少有个id属性
import { computed } from 'vue';
const props = defineProps<{
options: T[],
labelKey: keyof T, // 强制 labelKey 必须是 T 的一个键
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: T): void
}>();
const model = defineModel<T>(); // 假设使用 defineModel
function onSelect(option: T) {
model.value = option;
// emit('update:modelValue', option);
}
</script>
<template>
<ul>
<li v-for="option in options" :key="option.id" @click="onSelect(option)">
{{ option[labelKey] }}
</li>
</ul>
</template>
<!-- App.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import GenericSelect from './GenericSelect.vue';
type User = { id: number; name: string; email: string };
const users: User[] = [
{ id: 1, name: 'Alice', email: 'a@a.com' },
{ id: 2, name: 'Bob', email: 'b@b.com' }
];
const selectedUser = ref<User>();
// 当你把鼠标悬浮在 selectedUser.value 上时
// VSCode会告诉你它的类型是 User | undefined,而不是any!
// v-model 绑定和事件都能完美推断出类型
</script>
<template>
<GenericSelect
v-model="selectedUser"
:options="users"
label-key="name"
/>
<p v-if="selectedUser">Selected Email: {{ selectedUser.email }}</p>
</template>
核心思想: 泛型组件让你的组件从“只能处理特定形状的数据”进化为“能处理任何符合某种模式的数据”,同时保持了端到端的类型安全。这是TypeScript与Vue结合的终极魅力之一。
Trick 4: TS魔法 - as const
的妙用
场景痛点: 在项目中,我们经常定义一组固定的常量,比如Tabs的名称、API返回的状态码等。
// 普通定义
const TABS = ['profile', 'settings', 'security'];
type Tab = 'profile' | 'settings' | 'security'; // 我们需要手动维护这个类型
function setActiveTab(tab: Tab) { /* ... */ }
setActiveTab('billing'); // ❌ TS会报错,很好!
问题是,Tab
类型需要我们手动维护,如果 TABS
数组变了,Tab
类型忘了改,就会出问题。
解决方案: as const
as const
会告诉 TypeScript,这个对象/数组是“只读”的,并且它的值就是它字面的类型,而不是宽泛的 string
或 number
。
代码示例:
// 使用 as const
const TABS = ['profile', 'settings', 'security'] as const;
// TABS 的类型被推断为:readonly ["profile", "settings", "security"]
// 而不是 string[]
// 自动从常量生成类型!
type Tab = typeof TABS[number];
// Tab 的类型就是 'profile' | 'settings' | 'security'
// typeof TABS 获取 TABS 常量的类型
// [number] 是一个索引访问类型,表示获取该元组所有索引位上的值的联合类型
// 现在,Tab 类型与 TABS 常量完全同步,无需手动维护!
function setActiveTab(tab: Tab) { /* ... */ }
// 在组件 prop 中使用
defineProps<{
currentTab: Tab
}>();
核心思想:
as const
实现了“代码即文档,常量即类型”。它让你的常量定义成为单一数据源(Single Source of Truth),自动派生出精确的类型,消除了手动同步类型的繁琐和风险。
总结
shallowRef
:控制响应粒度,优化性能。readonly
:封装 Composable 状态,增强代码健壮性。generic
:打造类型安全的通用组件。as const
:自动化类型推断,减少维护成本。