Vue 3 Pinia 状态管理与响应式机制详解
状态变更的多种方式
在 Pinia 中,store 是通过 reactive 包裹的对象,因此直接访问属性时无需使用 .value。这与 Vue 3 setup 语法糖中的 props 行为一致,且不支持直接解构 store 对象。
单一属性修改
对于单个状态的更新,可以直接赋值。
<script setup>
import { usePlayerStore } from "@/stores/player";
const playerStore = usePlayerStore();
// 直接赋值
playerStore.volume = 80;
</script>
批量属性更新
当需要同时修改多个状态字段时,推荐使用 $patch 方法,这样可以确保触发正确的响应式更新。
<script setup>
import { usePlayerStore } from "@/stores/player";
const playerStore = usePlayerStore();
// 批量更新
playerStore.$patch({
volume: 80,
isMuted: false
});
</script>
复杂逻辑封装
如果状态更新涉及业务逻辑,应将其封装在 actions 中。这有助于维护代码的可读性和调试。
// stores/player.ts
import { defineStore } from "pinia";
const defaultState = () => ({
volume: 0,
error: {
message: "",
visible: false,
},
autoSave: false,
});
export const usePlayerStore = defineStore("player", {
state: defaultState,
actions: {
adjustVolume(step: number) {
// 包含逻辑的状态更新
this.volume += step;
if (this.autoSave) {
this.volume = this.volume % 100;
}
return "Volume Adjusted";
},
},
persist: true,
});
双向绑定与 Ref 转换
Vue 模板中的 v-bind 是单向的,而 v-model 需要双向绑定。由于 store 状态是响应式对象而非 ref,直接在 v-model 中使用可能会失效。此时需要使用 storeToRefs 将状态转换为 ref 对象。
<template>
<input v-model="playerRefs.volume" />
</template>
<script setup>
import { usePlayerStore } from "@/stores/player";
import { storeToRefs } from "pinia";
const store = usePlayerStore();
// 转换为 refs 以支持 v-model
const playerRefs = storeToRefs(store);
</script>
转换后,playerRefs.volume.value 与 store.volume 保持同步,修改前者等同于修改后者。
响应式引用与监听
在 JavaScript 中,基本类型(number, boolean, string)赋值通常为值传递,而对象和数组为引用传递。在 Pinia 中监听状态变化时,需注意这一点。
如果直接监听对象属性,可能无法触发回调。建议通过函数包裹或获取 ref 来确保监听有效性。
// 获取当前媒体项的引用
const getCurrentMedia = () => store.mediaList[store.currentId];
// 监听变化
watch(getCurrentMedia, (newMedia) => {
stopPlayback();
store.currentTime = 0;
loadInterface();
if (audioElement) startPlayback();
});
封装 Patch 逻辑
可以在 action 中封装 $patch 以便在更新前后执行额外逻辑,例如日志记录或副作用处理。
type StateShape = ReturnType<typeof defaultState>;
export const usePlayerStore = defineStore("player", {
state: defaultState,
actions: {
updateState(partial: Partial<StateShape>) {
// 更新前逻辑
console.log("Updating state...");
this.$patch(partial);
// 更新后逻辑
console.log("State updated");
}
}
});
Store 定义风格对比
Pinia 支持两种定义 store 的方式:选项式 API 和 组合式 API(Setup Store)。
选项式 API
- 结构类似配置对象,清晰明了。
- 推荐用于插件开发或需要严格 SSR 支持的场景。
- 支持多个 script 标签导出,便于逻辑聚合。
组合式 API (setup 语法)
- 写法更接近原生 TypeScript,适合项目初期快速开发。
- 变量作用域共享,灵活性高。
- 可以突破部分插件限制,但调试难度略高。
- 需熟悉 Vue 生命周期顺序:setup → template/style → 子组件。
高级实践:防抖持久化
在涉及本地存储或 IPC 通信(如 Tauri)时,频繁的状态变化可能导致性能问题。可以通过自定义 Watcher 类实现防抖和定期保存。
import { defineStore } from "pinia";
import { Ref, ref, reactive, toRefs, watch } from "vue";
import { Store } from "@tauri-apps/plugin-store";
const DEBOUNCE_MS = 1000;
const INTERVAL_MS = 5000;
const STORAGE_KEY = "app_data";
let storageInstance: Store | null = null;
export const useAudioStore = defineStore(
"audio",
() => {
const isTauriEnv = inject("isTauri") as boolean;
// 初始化存储实例
if (isTauriEnv) {
storageInstance = new Store("audio.bin");
}
async function persistData(val: any, key: string = STORAGE_KEY) {
if (isTauriEnv && storageInstance) {
return await storageInstance.set(key, val);
}
}
class DebouncedWatcher {
private debounceTimer?: NodeJS.Timeout | null;
private intervalTimer?: NodeJS.Timeout | null;
track(
source: Ref<any>,
callback?: Function | null,
key?: string
) {
return watch(source, (newVal) => {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.debounceTimer = setTimeout(() => {
if (this.intervalTimer) {
clearInterval(this.intervalTimer);
this.intervalTimer = null;
}
if (callback) callback(newVal, key);
}, DEBOUNCE_MS);
if (!this.intervalTimer) {
this.intervalTimer = setInterval(() => {
if (callback) callback(newVal, key);
}, INTERVAL_MS);
if (callback) callback(newVal, key);
}
});
}
}
const currentTime = ref(0);
const state = reactive({
playlist: [] as any[],
currentTrackId: 0,
selection: [] as number[],
loopMode: false,
shuffleMode: false,
error: {
message: "",
visible: false,
},
autoPersist: false,
});
const stateRefs = toRefs(state);
// 恢复数据
if (isTauriEnv && storageInstance) {
storageInstance.get(STORAGE_KEY).then((data) => {
if (data) {
Object.assign(state, data);
}
});
}
// 绑定防抖保存
const stopTimeWatch = new DebouncedWatcher().track(currentTime, persistData, "curTime");
const stopStateWatch = new DebouncedWatcher().track(currentTime, persistData);
return {
...stateRefs,
currentTime,
stopTimeWatch,
stopStateWatch,
};
},
{
persist: true,
}
);
注意,在 Setup Store 中必须返回 state 的所有属性,以确保 Pinia 能正确识别状态。私有属性或不返回的属性会影响 SSR hydration 及开发工具的正常运作。
此外,使用扩展运算符 {...obj} 会创建新对象,而 Object.assign 会修改原对象。在需要保持响应式引用时,应谨慎选择更新方式,避免破坏响应式链接。