Vue 状态管理
使用 Pinia
ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
options 选项式 store
store 仓库是缓存 state 状态的容器
ts
import { defineStore } from "pinia";
export const useUserStore /** 命名规范 useXxxStore */ = defineStore(
"user" /** storeId */,
{
state: () => ({
name: "username",
age: 18,
}),
// getters 类似 computed 计算属性, 会缓存计算结果
getters: {
userInfo: (state) => {
console.log("[useUserStore] computed userInfo:");
return `name: ${state.name}, age: ${state.age}`;
},
},
// actions 中可以写同步或异步方法
actions: {
resetUser() {
({ name: this.name, age: this.age } = { name: "username", age: 18 });
},
// actions 中不能写箭头函数, 否则没有 this
setAge: function (newAge: number) {
this.age = newAge;
},
setName: async function (newName: string) {
return new Promise((resolve) => {
setTimeout(() => {
this.name = newName;
resolve(newName);
}, 3000);
});
},
},
} /** options */,
);
vue
<script lang="ts" setup>
import { useUserStore } from "@/stores/user";
import { isProxy, isReactive, isRef } from "vue";
const userStore = useUserStore();
console.log(isRef(userStore), isReactive(userStore), isProxy(userStore)); // false true true
// 方式 1: 直接更新 state
const addAge = () => userStore.age++;
// 方式 2, 使用 store.$patch 更新部分 state
const patchUser = () => {
// store.$patch 可以接收部分 state
userStore.$patch({ age: userStore.age + 1 });
// store.$patch 也可以接收一个更新函数
userStore.$patch((state) => {
state.name += "!";
});
};
// 方式 3: 使用 store.$state 更新全部 state
const setUser = () => {
userStore.$state = {
age: userStore.age + 1,
name: userStore.name + "!",
};
};
const genAge = () => Math.floor(Math.random() * 100);
const genName = () => {
const arr = new Uint8Array(8);
crypto.getRandomValues(arr);
return Array.from(arr, (b) => b.toString(16).padStart(2, "0"))
.join("")
.slice(0, 8);
};
</script>
<template>
<div>name: {{ userStore.name }}, age: {{ userStore.age }}</div>
<!-- getters -->
<div>{{ userStore.userInfo }}</div>
<div class="flex flex-col gap-5">
<button @click="addAge">addAge</button>
<button @click="patchUser">patchUser</button>
<button @click="setUser">setUser</button>
<!-- 方式 4: 使用 actions 更新 state -->
<button @click="userStore.setAge(genAge())">setAge</button>
<button @click="userStore.setName(genName())">setName</button>
<button @click="userStore.resetUser()">resetUser</button>
</div>
</template>
storeToRefs
对 store 直接解构会失去响应式, 需要使用 vue 的 toRef
, toRefs
或 pinia 的 storeToRefs
解构
ts
// 直接解构会失去响应式
// const { age, name } = userStore;
// 需要使用 vue 的 toRef, toRefs 或 pinia 的 storeToRefs 解构
const { age, name } = storeToRefs(userStore);
store 仓库实例 api
store.$patch
更新部分 state, 可以接收部分 state, 也可以接收一个更新函数store.$state
更新全部 stateuserStore.$reset
重置 state 到初始值store.$subscribe
(类似 watch) 侦听 state 的改变, 改变 state 时, 调用传入的 callback; 返回停止侦听 (移除该 callback) 的函数store.$onAction
侦听 actions 的调用, 调用 actions 时, 调用传入的 callback; 返回停止侦听 (移除该 callback) 的函数
store.$subscribe
store.$subscribe((mutation, newState) => void)
ts
userStore.$subscribe(
(mutation, newState) => {
console.log(mutation, newState);
} /** callback */,
{
detached: false, // 默认 false, 组件卸载时移除 callback
// deep: true, // 默认 false, 深层侦听
immediate: false,
// 是否立即执行 callback
// 默认 false, 即默认懒执行 callback
flush: "pre", // "pre" | "post" | "sync", 默认 pre
// pre: state 更新前调用
// post: state 更新后调用 callback
// sync: 同步调用 callback
once: false, // 一次性侦听, callback 只调用一次
} /** options */,
);
store.$onAction
store.$onAction((context) => void)
context.after((actionReturnValue) => void /** callback */)
callback 的参数是 action 方法的返回值context.onError((err) => void /** callback */)
callback 的参数是 action 方法抛出的错误context.args
action 方法的参数数组context.store
store 仓库实例
ts
userStore.$onAction(
(context) => {
// callback 的参数是 action 方法的返回值
context.after(
(actionReturnValue) =>
console.log(
"[$onAction] context.after, actionReturnValue:",
actionRetVal,
) /** callback */,
);
// callback 的参数是 action 方法抛出的错误
context.onError(
(err) =>
console.log("[$onAction] context.onError, err:", err) /** callback */,
);
// action 方法的参数数组
console.log("[$onAction] context.args:", context.args);
// store 仓库实例
console.log(
"[$onAction] context.store === userStore:",
context.store === userStore,
);
},
false, // 默认 detached: false, 组件卸载时移除 callback
);
composition 组合式 store (setup 语法糖)
使用 setup 语法糖创建的 store 仓库实例没有 $reset 方法, 需要手动实现
ts
import { defineStore } from "pinia";
import { computed, reactive } from "vue";
export const useCntStore = defineStore("count", () => {
// state
const cnt = reactive({ v: 0 });
// getters (使用计算属性)
const cntInfo = computed(() => `count: ${cnt.v}`);
// actions
const resetCnt = () => {
cnt.v = 0;
};
const incCnt = () => cnt.v++;
const decCnt = () => cnt.v--;
// 使用 setup 语法糖创建的 store 仓库实例没有 $reset 方法, 需要手动实现
const $reset = () => {
resetCnt();
};
// 一定要 return!
return {
// state
cnt,
// getters (使用计算属性)
cntInfo,
// actions
incCnt,
decCnt,
$reset,
};
});
vue
<script lang="ts" setup>
import { useCntStore } from "@/stores/count";
import { storeToRefs } from "pinia";
const cntStore = useCntStore();
const { cnt, cntInfo } = storeToRefs(cntStore);
const { incCnt, decCnt } = cntStore;
const addCnt = () => cntStore.cnt.v++;
const subCnt = () => cnt.value.v--;
</script>
<template>
<div>cntStore.cnt: {{ cntStore.cnt }}</div>
<div>cnt: {{ cnt }}</div>
<div>cntStore.cntInfo: {{ cntStore.cntInfo }}</div>
<div>cntStore.cnt: {{ cntInfo }}</div>
<div class="flex flex-col gap-5">
<button @click="addCnt">addCnt</button>
<button @click="subCnt">subCnt</button>
<button @click="incCnt">incCnt</button>
<button @click="decCnt">decCnt</button>
<button @click="cntStore.$reset()">$reset</button>
</div>
</template>
Pinia 持久化
对比 localStorage 和 sessionStorage
- localStorage: 数据存储到磁盘, 没有过期时间
- sessionStorage: 数据缓存到内存, 会话结束时自动清除
页面刷新后, store 仓库缓存的 state 状态丢失
- 对于选项式 store 仓库实例, 调用
toRaw(store.$state)
将 $state 转换为普通对象 - 对于组合式 store 仓库实例, 必须递归调用
toRaw(store.$state)
将 $state 转换为普通对象
js
import { isRef, unref } from "vue";
export function deepToRaw(observed) {
const isObj = (val) => val !== null && typeof val === "object";
// unref(obj)
// 如果 obj 是 ref 创建的响应式对象, 则返回 obj.value, 否则直接返回 obj
const val = isRef(observed) ? unref(observed) : observed;
if (!isObj(val)) {
return val;
}
if (Array.isArray(val)) {
const rawArr = [];
val.forEach((item) => {
rawArr.push(deepToRaw(item));
});
return rawArr;
}
const rawObj = {};
Object.keys(val).forEach((key) => {
rawObj[key] = deepToRaw(val[key]);
});
return rawObj;
}
ts
import { deepToRaw } from "@/utils/deepToRaw";
import { createApp } from "vue";
import { createPinia, type PiniaPlugin, type PiniaPluginContext } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const setLocalStorage = (key: string, value: unknown) => {
const rawVal = deepToRaw(value);
localStorage.setItem(key, JSON.stringify(rawVal));
};
const getLocalStorage = (key: string) =>
JSON.parse(localStorage.getItem(key) ?? "{}");
const piniaPluginPersist = (): PiniaPlugin => {
return (ctx: PiniaPluginContext) => {
const key = ctx.store.$id;
const val = getLocalStorage(key);
console.log(`[piniaPluginPersist] key: ${key}, value: ${val}`);
// store.$subscribe
// (类似 watch) 侦听 state 的改变, 改变 state 时, 调用传入的 callback; 返回停止侦听 (移除该 callback) 的函数
ctx.store.$subscribe(() =>
setLocalStorage(key, deepToRaw(ctx.store.$state)),
);
return val;
};
};
const pinia = createPinia();
pinia.use(piniaPluginPersist());
app.use(pinia);
app.mount("#app");