vue3 开发技巧

0 阅读5分钟
  1. 性能调优: shallowReftriggerRef 的精妙配合
  2. 架构模式:readonly 打造“防弹”的 Composables
  3. 类型体操:generic 让你的组件“活”起来
  4. 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 事件都能返回正确的、带有完整类型信息的对象,而不是 anyobject

解决方案:<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,这个对象/数组是“只读”的,并且它的值就是它字面的类型,而不是宽泛的 stringnumber

代码示例:

// 使用 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:自动化类型推断,减少维护成本。