Vue3.2: Pinia的用法之妙
总结(倒叙写法)
根据源码的依据,以及文档的总结,Pinia是同时兼容 Vue2
和 Vue3
那么我们为什么要使用它呢,或者说什么时候才需要使用它? 相信这是每一个刚接触到这个Pinia名词的时候,都带着的疑问
官网给出的解释:Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态, 哈哈,概念很明显,这不正是我们在 Vue2
所使用的 Vuex
的功用一致吗。那就不做过多的解读了。
当然升级版本,就像 Vue2 升级到 Vue3 所带来的诸多好处一样。Vuex
换成 Pinia
,总有其所备受推崇的理由
这里就说下我所知道的关于 Pinia
的优点:
-
兼容 Vue2 和 Vue3
-
抛弃传统的
Mutation
,只有state, getter
和action
,简化状态管理库 -
不需要嵌套 modules , 使用 Composition api (Pinia 最初是在 2019 年 11 月左右重新设计使用 Composition API)
-
TypeScript支持 (不需要自个添加TS包装器)
-
扁平化设计,没有 命名空间模块。鉴于 Store 的扁平架构,“命名空间” Store 是其定义方式所固有的,也可以说是所有 Store 都是命名空间的
-
待JYM的帮补充......
回忆Vuex
挪用官网的截图
从图中可以看出,Action、Mutations、State共同组成了
Vuex
全局单例模式管理系统
简略说下Action、Mutations、State的作用
State
负责存储整个应用的状态数据Mutations
的中文意思是“变化”,利用它可以更改状态,本质就是用来处理数据的函数Actions
也可以用于改变状态,不过是通过触发mutation实现的,重要的是可以包含异步操作
他们的组合形成了一个store对象,可以把它看作是一个容器,如下面代码的样子
const mutations = {...};
const actions = {...};
const state = {...};
Vuex.Store({
state,
actions,
mutation
});
我们都知道,Vuex的状态,在刷新页面后会清理内存,数据会丢失,后面我们通过了两种方案,解决了这个问题,分别是:
- 一、localStorage存储、监听,回显,刷新页面的时候重新赋值给vuex,类似于初始化的作用
- 二、利用第三方库进行持久化存储,
vuex-persistedstate
疑问Vue3后Pinia数据丢失?
那么我想问,使用Pinia,当页面刷新的时候,是否也会丢失数据呢?
看到这里的JYM可能会想,既然提出了这个问题,应该是跟vuex是有反差的,也就是觉得不会丢失数据/
其实,pinia
的状态与vuex一样把数据存在内存中,也就是,会和Vuex遇到同样的问题,而且其解决方案也是异曲同工
在状态改变时将其同步到浏览器的存储中,如 cookie
、localStorage
、sessionStorage
。
可以搭配 pinia-persistedstate-plugin
插件来实现持久化,原理也是把数据存入localStorage中,只是插件会帮助自动存入与取出,不需要自己去操作localstorage
。
计划:后续写一篇关于该插件使用方式的总结
Pinia的改进
从总结中,说到Pinia摒弃了mutation
为什么Pinia不需要mutation了?
我从开始接触Vuex的时候,就觉得mutation特别繁琐,当时就在想,为啥一定要将同步和异步的方法分开呢?
然后我带着疑问查阅资料和翻看源码,大致是这样子:
vuex中的Mutation真的没必要吗?
在 vuex 里面 actions 只是一个架构性的概念,这个函数想实现什么同步或者异步看你自己的需求,并不作限制,但是若想改变state的状态,需要通过mutation去触发。vuex
真正限制的只有 mutation
必须是同步的这一点。其实是为了能用 devtools 追踪状态变化,
同步的意义在于每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools
就可以打个 snapshot
存下来,然后就可以随便 time-travel
了。如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。
这样一分析,在Vuex中使用mutation并不是完全没有必要的。
到了Pinia, 将同步异步,统一合并到了action里面,并区分判断同步异步,看下面的源码
const partialStore = {
_p: pinia,
// _s: scope,
$id,
$onAction: addSubscription.bind(null, actionSubscriptions),
$patch,
$reset,
$subscribe(callback, options = {}) {
const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher());
const stopWatcher = scope.run(() => vueDemi.watch(() => pinia.state.value[$id], (state) => {
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback({
storeId: $id,
type: exports.MutationType.direct,
events: debuggerEvents,
}, state);
}
}, assign({}, $subscribeOptions, options)));
return removeSubscription;
},
$dispose,
};
Store的定义方式
非composition API定义方式
export const useCounterVue2Store = defineStore('counterVue2', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
incrementAsync() {
}
},
persist: {
key: 'counterVue2',
storage: localStorage
}
})
保留了state、action以前的部分方式
composition API定义方式
export const useCounterStore = defineStore('counter',
() => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
},
{
persist: {
key: 'userCounter',
storage: localStorage
}
}
)
Store中使用getter
也许这里有人会有困惑,getter每次调用一次,是不是就要计算一次表达公示,比如下面的代码。
其实不然,跟computed一样,当里面依赖的变量不发生改变的时候,是不会重新计算的,它们有数据缓存机制,可以自行测试一下,将getter写成一个方法,然后从中做个断点或者打印值就知道了。
getters: {
double: (state) => state.count * 2,
}
getter中调用其它getter
可以直接在getter方法中调用this,this指向的便是store实例, 这里要注意,就不能使用箭头函数了,关于this指向的问题
export const useCounterVue2Store = defineStore('counterVue2', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
doubleTwo(): number {
return this.double + 22
}
},
actions: {
increment() {
this.count++
},
incrementAsync() {
}
}
})
getter中调用其它store的getter
getters: {
double: (state) => state.count * 2,
doubleTwo(): number {
return this.double + 22
},
// 调用其他store的getter
doubleAddOtherStoreGetter: (state) => {
const otherCounter = useCounterStore()
return otherCounter.count + state.count + 444
}
}
getter传参的实现
按照官网的说法,Getters 只是幕后的 computed 属性,因此无法向它们传递任何参数。
但是,可以从 getter 返回一个函数以接受任何参数:
getters: {
double: (state) => state.count * 2,
doubleTwo(): number {
return this.double + 22
},
// 调用其他store的getter
doubleAddOtherStoreGetter: (state) => {
const otherCounter = useCounterStore()
return otherCounter.count + state.count + 444
},
getFunctionVoid: (state) => {
const otherCounter = useCounterStore()
return (val: number) => {
return 1100 + otherCounter.count
}
}
}
Store的一些函数
Store的$patch
$patch(第一个参数支持一个对象/函数)
如下:
对象
store.$patch({
count: counter.count + 22,
});
函数
store.$patch(
(state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
});
Store的$subscribe
可以通过 store 的 $subscribe()
方法查看状态及其变化,类似于 Vuex 的 subscribe 方法。 与常规的 watch()
相比,使用 $subscribe()
的优点是 subscriptions 只会在 patches 之后触发一次,意思就是每次执行$patch的时候,会触发一次。
此订阅默认组建销毁后被自动删除,若想保留可{ detached: true }
作为第二个参数传递$subscribe
使用过程遇到的bug
使用store.$reset()重置数据
Uncaught Error: 🍍: Store "counter" is built using the setup syntax and does not implement $reset().
解决方法:
import { createPinia } from 'pinia';
import { createPersistedState } from 'pinia-plugin-persistedstate';
const store = createPinia();
store.use(({ store }) => {
const initialState = JSON.parse(JSON.stringify(store.$state));
store.$reset = () => {
store.$state = JSON.parse(JSON.stringify(initialState));
}
});
当然这种使用JSON.Stringify的解决方案会存在一定的问题
- 使用JSON.Stringify 转换的数据中,如果包含 function,undefined,Symbol,这几种类型,不可枚举属性, JSON.Stringify序列化后,这个键值对会消失。
- 转换的数据中包含 NaN,Infinity 值(含-Infinity),JSON序列化后的结果会是null。
- 转换的数据中包含Date对象,JSON.Stringify序列化之后,会变成字符串。
- 转换的数据包含RegExp 引用类型序列化之后会变成空对象。
- 无法序列化不可枚举属性。
- 无法序列化对象的循环引用,(例如: obj[key] = obj)。
- 无法序列化对象的原型链
附带查阅: Pinia中文文档
转载自:https://juejin.cn/post/7210681364470988861