🌏 Vue 状态管理
启动
概念
Store
:不与组件树绑定,承载着全局状态(数据)。在Vue
中有vuex
,pinia
;在React
中,有redux
、unstate-next
🤔 并非所有应用都需要全局状态
🏞️ Store 的使用场景:在整个应用中可访问到的数据。例如:
避免将本可以保存至组件中的数据保存到 Store,例如一个元素在页面的可见性
对于pinia
,有state、getter、action三个概念
可理解为对应组件的
data
-> state
computed
-> getter
methods
-> action
在 setup 中的对应关系是
state
-> ref()
getter
-> computed()
action
-> function()
这三个概念可以认为是:值、通过值得到的、用来改变值的
安装
通过 Vite
创建一个 Vue
项目
安装pinia
习惯在相对项目根目录创建src/store/index.ts
文件,以此作为入口来配置整个项目的 _store_(state??)
1 2 3 4 5
| import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;
|
在vue
的入口文件main.ts
中导入并使用
1 2 3 4 5 6 7 8
| import { createApp } from "vue"; import App from "./App.vue"; import pinia from "./store";
const app = createApp(App);
app.use(pinia); app.mount("#app");
|
如果是在Vue2
中
1 2 3 4 5 6 7 8
| import { createPinia, PiniaVuePlugin } from "pinia";
Vue.use(PiniaVuePlugin); new Vue({ el: "#app", pinia, });
|
快速使用
在 setup 中使用
定义
在setup
中使用非常方便,写法和普通的 JavaScript 大差不差
使用defineStore
定义一个Store
:(可在src/store
下创建新的文件)
1 2 3 4 5 6 7 8
| export const useUserStore = defineStore("user", () => { const username = ref(""); const setUsername = (name: string) => { username.value = name; };
return { username, setUsername }; });
|
🎇 Store 的名字推荐是useXXXStore
- 第一个参数是 Store 的唯一 ID
- 第二个参数是一个函数
使用
在 Vue 组件的<script setup>
中
1 2 3 4 5 6 7 8 9 10
| import { useUserStore } from "xx/store/userStore";
const { username, setUsername } = useUserStore();
setUsername("Jerry");
console.log(username); console.log(useUserStore().username);
|
要保持响应性,可使用storeToRefs
1 2 3 4 5 6
| import { storeToRefs } from "pinia";
const store = useUserStore(); const { username } = storeToRefs(store);
const { setUsername } = store;
|
如果有报错说类似是:setUsername is not a function
的,重启一下项目就好了。?不知道是 vite 的问题还是 pinia 的问题
不在 setup 中使用
1 2 3 4 5 6 7 8 9 10 11
| export const useCounterStore = defineStore("counter", { state: () => ({ count: 0 }), getters: { double: (state) => state.count * 2, }, actions: { increment() { this.count++; }, }, });
|
使用方法和在setup
中一样,就像是Vue
中Option API和Composition API的区别
细节使用
state
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { defineStore } from "pinia";
const useStore = defineStore("storeId", { state: (): State => { return { count: 0, items: [] as ItemInfo[], }; }, });
interface ItemInfo { id: number; name: string; }
interface State { count: string; items: array; }
|
修改
使用的时候可以直接进行读写来改变 store,也可以使用$patch
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const store = useStore();
store.count++;
store.$patch({ count: store.count + 1, items: [{ id: 0, name: "huawei" }], });
store.$patch((state) => { state.items.push({ name: "shoes", quantity: 1 }); state.hasChanged = true; });
store.$reset();
|
替换
不能完全替换掉store 的 state,可以 patch 它们
1 2 3 4
| store.$state = { count: 24 };
store.$patch({ count: 24 });
|
可以通过变更pinia
实例的state
来设置整个应用的初始 state。常用于SSR 中的激活过程(为了安全起见,pinia.state.value
被转义为其他形式)
订阅 State
$subscribe
监听state
的变化,相比于watch
,好处是 subscriptions (订阅的内容)在 patch 后只触发一次
1 2 3 4 5 6 7 8 9 10 11
| cartStore.$subscribe((mutation, state) => { mutation.type; mutation.storeId; mutation.payload;
localStorage.setItem("cart", JSON.stringify(state)); });
|
state subscription 会被绑定到添加它们的组件上,组件被卸载的时候,这些订阅会被删除。添加{detachd: true}
作为第二个参数将订阅从当前组件中分离
1 2 3 4 5
| <script setup> const someStore = useSomeStore(); // 此订阅器即便在组件卸载之后仍会被保留 someStore.$subscribe(callback, { detached: true }); </script>
|
也可以在pinia实例上监听整个 state
1 2 3 4 5 6 7 8
| watch( pinia.state, (state) => { localStorage.setItem("piniaState", JSON.stringify(state)); }, { deep: true } );
|
在 OptionAPI 的用法
1 2 3 4 5 6 7 8 9 10
|
import { defineStore } from "pinia";
const useCounterStore = defineStore("counter", { state: () => ({ count: 0, }), });
|
使用mapState()
辅助函数将 state 属性映射为只读的计算属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { mapState } from 'pinia' import { useCounterStore } from '../stores/counter'
export default { computed: { ...mapState(useCounterStore, ['count']) ...mapState(useCounterStore, { myOwnName: 'count', double: store => store.count * 2, magicValue(store) { return store.someGetter + this.count + this.double }, }), }, }
|
使用mapWritableState()
辅助函数将 state 映射为可修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { mapWritableState } from 'pinia' import { useCounterStore } from '../stores/counter'
export default { computed: { ...mapWritableState(useCounterStore, ['count']) ...mapWritableState(useCounterStore, { myOwnName: 'count', }), }, }
|
Getter
- 完全等同于 state 的计算属性。推荐使用箭头函数,接收一个 state 作为第一个参数
- 在
setup
中,就是通过 state 返回衍生值的那个方法(不传参)
- 不可异步
1 2 3 4 5 6 7 8
| export const useStore = defineStore("main", { state: () => ({ count: 0, }), getters: { doubleCount: (state) => state.count * 2, }, });
|
访问其他 Getter
除了依赖 state,也可以访问到其他的 getter
在使用TypeScript
的时候,如果是通过this
访问其他 getter,需要明确当前 getter 的返回类型(ts 的问题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export const useStore = defineStore("main", { state: () => ({ count: 0, }), getters: { doubleCount(state) { return state.count * 2; }, doublePlusOne(): number { return this.doubleCount + 1; }, }, });
|
- 访问另外一个 store(另外的
defineStore
) 的 Getter,也是一样的操作
传参
计算属性不传参的,不过可以让 getter 返回一个函数,这个函数可以接收任意参数
1 2 3 4 5 6 7
| export const useStore = defineStore("main", { getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId); }, }, });
|
- 这样的 getter 不会被缓存(计算属性缓存),但性能会好点
在setup
中使用
作为 store 的一个属性,可直接访问
1 2 3 4 5
| <script setup> const store = useCounterStore(); store.count = 3; store.doubleCount; // 6 </script>
|
在 OptionAPI 中的用法
这里面也分为两种风格:
- 组合式 API,不是在
<script setup>
里面,是在defineComponent
里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script> import { useCounterStore } from '../stores/counter'
export default defineComponent({ setup() { const counterStore = useCounterStore()
return { counterStore } }, computed: { quadrupleCounter() { return this.counterStore.doubleCount * 2 }, }, }) </script>
|
- OptionAPI 的形式(和 state 一样,使用辅助函数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { mapState } from "pinia"; import { useCounterStore } from "../stores/counter";
export default { computed: { ...mapState(useCounterStore, ["doubleCount"]), ...mapState(useCounterStore, { myOwnName: "doubleCount", double: (store) => store.doubleCount, }), }, };
|
Action
- 相当于组件的
method
- 在
setup
中,就是改变 state 的那个方法
- 可以是异步的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { mande } from "mande";
const api = mande("/api/users");
export const useUsers = defineStore("users", { state: () => ({ userData: null, }),
actions: { async registerUser(login, password) { try { this.userData = await api.post({ login, password }); showTooltip(`Welcome back ${this.userData.name}!`); } catch (error) { showTooltip(error); return error; } }, }, });
|
订阅 action
通过store.$onAction()
来监听 action 和它的结果;传递给它的回调函数会在 action 本身之前执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const unsubscribe = someStore.$onAction( ({ name, // 使用到的 action 名称 store, // store 实例,类似 `someStore` args, // 传递给 action 的参数数组 after, // 在 action 返回或解决后的钩子 onError, // action 抛出或拒绝的钩子 }) => { const startTime = Date.now(); console.log(`Start "${name}" with params [${args.join(", ")}].`);
after((result) => { console.log( `Finished "${name}" after ${ Date.now() - startTime }ms.\nResult: ${result}.` ); });
onError((error) => { console.warn( `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.` ); }); } );
unsubscribe();
|
- 订阅默认绑定在添加它们的组件内,组件卸载时,订阅也会自动取消
如果不想让订阅跟着组件取消,将true
传递给第二个参数
1 2 3 4 5
| <script setup> const someStore = useSomeStore(); // 此订阅器即便在组件卸载之后仍会被保留 someStore.$onAction(callback, true); </script>
|
在 OptionAPI 中的用法
- 使用
setup()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script> import { useCounterStore } from '../stores/counter' export default defineComponent({ setup() { const counterStore = useCounterStore() return { counterStore } }, methods: { incrementAndPrint() { this.counterStore.increment() console.log('New Count:', this.counterStore.count) }, }, }) </script>
|
- 不使用
setup()
,也是使用辅助函数
1 2 3 4 5 6 7 8 9 10 11 12
| import { mapActions } from 'pinia' import { useCounterStore } from '../stores/counter'
export default { methods: { ...mapActions(useCounterStore, ['increment']) ...mapActions(useCounterStore, { myOwnName: 'increment' }), }, }
|
插件
插件是一个函数,通过pinia.use()
添加到 pinia 实例中,可以选择性地返回要添加到 store 的属性
1 2 3 4 5 6 7 8 9 10 11 12
| import { createPinia } from "pinia";
const pinia = createPinia();
pinia.use(() => { return { secret: "the cake is a lie" }; });
const store = useStore(); store.secret;
|
- 所以在创建全局(共享)的 store 变量时很有用
- 每个 store 都被
reactive
包装过,所以对于ref
的值也无需使用.value
插件函数有一个可选的context
参数
1 2 3 4 5 6 7
| export function myPiniaPlugin(context) { context.pinia; context.app; context.store; context.options; }
|
因为插件本身是一个函数,所以在pinia.use
的时候也可以传递参数进去
🤔 第一个参数是一个context,要想接收到传递过来的参数,需要使用函数柯里化
- 在外面再包一层函数用来接收传递过来的参数
- 然后返回一个函数,作为参数传递给
pinia.use
1 2 3 4 5
| export function myPlugin(options) { return (context) => { }; }
|
使用插件
1 2 3
| import { myPlugin } from "xx.js";
pinia.use(myPlugin({ msg: "hello pinia" }));
|
在 TypeScript 中使用时可以指定options
的类型,然后再使用的时候会有类型检测
1 2 3 4 5 6 7 8 9
| interface Options { msg: string; }
export function myPlugin(options: Options) { return (context) => { }; }
|
在使用的时候传递不符合Options
时会报错