Vue3 Ref、Reactive、Computed 類型
響應式是 Vue 的核心。本篇將深入介紹 Ref、Reactive、Computed 的類型標註細節。
一、 Ref 類型詳解
1.1 基本類型
typescript
import { ref, type Ref } from 'vue';
// 自動推論
const count = ref(0); // Ref<number>
const name = ref('John'); // Ref<string>
const active = ref(true); // Ref<boolean>
// 明確標註
const age = ref<number>(25);
const title = ref<string>('Hello');1.2 允許 null
typescript
interface User {
id: number;
name: string;
}
// 初始為 null
const user = ref<User | null>(null);
// 之後賦值
user.value = { id: 1, name: 'John' };
// 存取需注意 null
if (user.value) {
console.log(user.value.name);
}
// 或使用可選鏈
console.log(user.value?.name);1.3 陣列類型
typescript
interface Todo {
id: number;
text: string;
done: boolean;
}
// 空陣列需明確標註
const todos = ref<Todo[]>([]);
// 有初始值可推論
const numbers = ref([1, 2, 3]); // Ref<number[]>
// 添加元素
todos.value.push({ id: 1, text: 'Learn TS', done: false });1.4 Ref 類型
typescript
import { ref, type Ref } from 'vue';
// 使用 Ref 類型
function useCounter(): Ref<number> {
return ref(0);
}
// 解包類型
import { type UnwrapRef } from 'vue';
type Unwrapped = UnwrapRef<Ref<number>>; // number二、 Reactive 類型詳解
2.1 基本用法
typescript
import { reactive } from 'vue';
// 自動推論
const state = reactive({
count: 0,
name: 'John',
});
// { count: number, name: string }
// 明確標註
interface State {
count: number;
name: string;
user: User | null;
}
const state2 = reactive<State>({
count: 0,
name: 'John',
user: null,
});2.2 避免的寫法
typescript
// 不要用 ref 包裝 reactive
const state = ref(reactive({ count: 0 })); // 不必要
// 不要解構 reactive
const { count } = reactive({ count: 0 });
// count 失去響應性!
// 正確做法:使用 toRefs
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0, name: 'John' });
const { count, name } = toRefs(state);
// count 和 name 是 Ref2.3 巢狀物件
typescript
interface Address {
city: string;
country: string;
}
interface User {
name: string;
address: Address;
}
const user = reactive<User>({
name: 'John',
address: {
city: 'Taipei',
country: 'Taiwan',
},
});
// 巢狀屬性也是響應式
user.address.city = 'Kaohsiung';三、 Computed 類型詳解
3.1 唯讀 computed
typescript
import { ref, computed, type ComputedRef } from 'vue';
const count = ref(0);
// 自動推論
const double = computed(() => count.value * 2);
// ComputedRef<number>
// 明確標註
const formatted = computed<string>(() => {
return `Count: ${count.value}`;
});
// 使用 ComputedRef 類型
const result: ComputedRef<number> = computed(() => count.value * 2);3.2 可寫入 computed
typescript
import { ref, computed, type WritableComputedRef } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName: WritableComputedRef<string> = computed({
get(): string {
return `${firstName.value} ${lastName.value}`;
},
set(value: string) {
const parts = value.split(' ');
firstName.value = parts[0] || '';
lastName.value = parts[1] || '';
},
});
// 讀取
console.log(fullName.value);
// 寫入
fullName.value = 'Jane Smith';3.3 依賴其他 computed
typescript
const count = ref(0);
const double = computed(() => count.value * 2);
const quadruple = computed(() => double.value * 2);
// 類型自動推論
// double: ComputedRef<number>
// quadruple: ComputedRef<number>四、 toRef 和 toRefs
4.1 toRef
typescript
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0,
name: 'John',
});
// 將單個屬性轉成 ref
const countRef = toRef(state, 'count');
// Ref<number>
// 修改會同步
countRef.value++;
console.log(state.count); // 14.2 toRefs
typescript
import { reactive, toRefs } from 'vue';
interface State {
count: number;
name: string;
}
const state = reactive<State>({
count: 0,
name: 'John',
});
// 將所有屬性轉成 ref
const refs = toRefs(state);
// { count: Ref<number>, name: Ref<string> }
// 解構後保持響應性
const { count, name } = toRefs(state);4.3 組合函式回傳
typescript
import { reactive, toRefs } from 'vue';
function useUser() {
const state = reactive({
name: 'John',
age: 30,
});
// 回傳 toRefs 以保持響應性
return toRefs(state);
}
// 使用
const { name, age } = useUser();
// name: Ref<string>, age: Ref<number>五、 shallowRef 和 shallowReactive
5.1 shallowRef
typescript
import { shallowRef, type ShallowRef } from 'vue';
interface DeepObject {
nested: { value: number };
}
const data: ShallowRef<DeepObject> = shallowRef({
nested: { value: 0 },
});
// 修改 .value 會觸發更新
data.value = { nested: { value: 1 } };
// 修改深層屬性不會觸發
data.value.nested.value = 2; // 不會更新視圖
// 手動觸發
import { triggerRef } from 'vue';
triggerRef(data);5.2 shallowReactive
typescript
import { shallowReactive } from 'vue';
const state = shallowReactive({
count: 0,
nested: { value: 0 },
});
// 第一層是響應式
state.count = 1; // 觸發更新
// 巢狀不是響應式
state.nested.value = 1; // 不觸發更新六、 readonly
6.1 基本用法
typescript
import { reactive, readonly } from 'vue';
const original = reactive({ count: 0 });
const readonlyCopy = readonly(original);
// 只能讀取
console.log(readonlyCopy.count);
// 嘗試修改會報錯(開發模式)
// readonlyCopy.count = 1 // 警告!
// 原始物件修改會反映
original.count = 1;
console.log(readonlyCopy.count); // 16.2 類型
typescript
import { readonly, type DeepReadonly } from 'vue';
interface State {
count: number;
nested: { value: number };
}
const state = reactive<State>({
count: 0,
nested: { value: 0 },
});
const readonlyState: DeepReadonly<State> = readonly(state);七、 實用範例
7.1 表單狀態
typescript
import { reactive, computed } from 'vue';
interface FormState {
username: string;
email: string;
password: string;
}
interface FormErrors {
username?: string;
email?: string;
password?: string;
}
const form = reactive<FormState>({
username: '',
email: '',
password: '',
});
const errors = reactive<FormErrors>({});
const isValid = computed(() => {
return !errors.username && !errors.email && !errors.password;
});
function validate() {
errors.username = form.username.length < 3 ? '至少 3 個字元' : undefined;
errors.email = !form.email.includes('@') ? '無效的 email' : undefined;
errors.password = form.password.length < 6 ? '至少 6 個字元' : undefined;
}7.2 API 狀態
typescript
import { ref, computed } from 'vue';
interface User {
id: number;
name: string;
}
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<Error | null>(null);
const hasUsers = computed(() => users.value.length > 0);
const firstUser = computed(() => users.value[0] ?? null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/users');
users.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}總結
| API | 用途 | 類型 |
|---|---|---|
ref<T>() | 單值響應式 | Ref<T> |
reactive<T>() | 物件響應式 | T |
computed<T>() | 計算屬性 | ComputedRef<T> |
toRef() | 單屬性轉 Ref | Ref<T> |
toRefs() | 全屬性轉 Refs | { [K]: Ref<T[K]> } |
readonly() | 唯讀 | DeepReadonly<T> |
> **選擇建議**:
- 原始值用
ref - 物件/陣列用
reactive - 解構用
toRefs
進階挑戰
- 實作一個
useToggle組合函式 - 建立一個帶有 loading/error 狀態的
useRequest<T> - 實作一個
useDebounceRef<T>