前言
两年的时间都在写 react, 太久没有接触 vue 的项目了,对于其的生态也变的更加的陌生了。
比如:
- 以前的 vuex,针对 vue3 出来,有所变动,没有了解过。
- 新出来的 pinia,更是没有使用过(幸好,还听说过,哈哈)
针对自己的不熟悉东西,去多掌握掌握,总是没有坏处的,万一哪一天,我开始写 vue 了呢?总不至于尴尬吧!
今天清明节,学习 vue 的状态管理库: vuex 和 pinia。
下面是自己的学习笔记,有误多多指教
vuex
采用中文官方的概念解释:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
vuex 开发流程架图
上图就是针对开发人员使用 vuex 的开发流程,还是很明确的。
vuex 的核心概念
vuex 核心使用要点
安装
pnpm install vuex
定义 store 实例对象,全局注册
// store/index.js
import { createStore } from 'vuex'
const store = createStore({})
// main.js
const app = createApp(App)
app.use(store) // 中间件,全局注册store
对象或者函数形式都可以,因为实现数据共享,就算是同一个内存地址也没有关系
/**
* =================================
* 定义
* =================================
*/
const store = createStore({
state() { // 函数形式
return {
name: 'copyer'
}
}
})
/**
* =================================
* 使用
* =================================
*/
// 方式一:在 template 模板中使用
<div> {{ $store.state.name }}</div>
// 方式二:在 <script> 中使用
import { useStore } from 'vuex'
const store = useStore()
console.log(store.state.name)
/**
* =================================
* 辅助函数
* =================================
*/
// 辅助函数,就是为了防止多次重复书写(这里只针对 options api 的使用,针对 composition api 会在后面封装后使用)
import { mapState } from 'vuex'
export default {
computed: {
...mapState({name: state => state.name})
}
}
/**
* =================================
* 模块中的state
* =================================
*/
// 假设模块名为 home: { name: 'home'}
// template 中使用
<div> {{ $store.state.home.name }}</div>
// 辅助函数
import { mapState } from 'vuex'
export default {
computed: {
// 第一个参数为模块名 namespace
...mapState('home', {name: state => state.name})
}
}
注意一下:辅助函数本质上就是一个函数,执行之后返回一个对象( key-value 的形式)。
key: string
value: function
还有种情况就是辅助函数接受一个数组,其内部对数组进行了转化(调用了
normalizeMap函数),也会转化成一个对象的形式。这里需要先关注一下,后面的辅助函数封装,理解这里是非常必要的。
/**
* =================================
* 定义与使用
* 默认参数
* =================================
*/
const store = createStore({
...
getters: {
/**
* 名字变成大写
* @param state 实例 store 的 state
* @param getters 实例 store 的 getters
*/
getNameToUpperCase(state, getters) {
return state.name.toLocaleUpperCase();
},
},
});
// 使用
<h1>{{ $store.getters.getNameToUpperCase }}</h1>
/**
* =================================
* 接受参数
* =================================
*/
const store = createStore({
...
getters: {
// 返回一个函数,来接受参数
getNameJoinStr(state) {
return (str) => {
return state.name + '' + str
}
}
},
});
<h1>{{ $store.getters.getNameJoinStr('_name') }}</h1>
/**
* =================================
* 辅助函数: 与 mapState 用法一致
* =================================
*/
/**
* =================================
* 使用:this.$store / useStore
* 函数触发,参数传递
* =================================
*/
const store = createStore({
...
mutations: {
/**
* 改变 name
* @param state 实例 store 的 state
* @param payload 传递的参数
*/
changeName(state, payload) {
state.name = payload.name
}
}
});
// 使用
// options api
export default {
methods: {
btn() {
// 两种写法
this.$store.commit('changeName', {name: 'james'})
this.$store.commit({type: 'changeName', name: 'james'})
// 针对模块中的 changeName 方法调用
this.$store.commit('home/changeName', {name: 'james'})
}
}
}
// composition api
const store = useStore()
store.commit('changeName', {name: 'james'}) // 跟上面一样,也是两种写法
// 都是一样的道理
/**
* =================================
* 辅助函数
* =================================
*/
import { mapMutations} from 'vuex'
export default {
methods: {
...mapMutations({
change: 'changeName', // 映射方法,会直接映射到 this.$store.commit('changeName')
})
// 针对模块也是一样的
...mapMutations('home', {
changeHome: 'changeName'
})
}
}
/**
* =================================
* 参数解析
* =================================
*/
const store = createStore({
...
actions: {
/**
* 异步操作,触发mutations,修改state
* @param context {commit, dispatch, getters, rootGetters, rootState, state}
* @param payload 携带的参数
*/
asyncChangeName(context, payload) {
setTimeout(() => {
context.commit("changeName", payload);
}, 2000);
},
},
});
/**
* =================================
* 使用:this.$store / useStore
* 函数触发,参数传递
* =================================
*/
// 使用
// options api
export default {
methods: {
btn() {
// 两种写法
this.$store.dispatch('asyncChangeName', {name: 'james'})
this.$store.dispatch({type: 'asyncChangeName', name: 'james'})
// 针对模块中的 changeName 方法调用
this.$store.dispatch('home/asyncChangeName', {name: 'james'})
}
}
}
// composition api
const store = useStore()
store.dispatch('asyncChangeName', {name: 'james'}) // 跟上面一样,也是两种写法
/**
* =================================
* 辅助函数:跟 mapMutations 的使用基本一致
* =================================
*/
没有 namespaced 的模块合并
// 针对 state 的合并(以 home 模块为例)
state() {
return {
// 这里的合并还是存在层级关系的
name: 'copyer',
home: {
name: 'home'
}
}
}
// 针对 getters, mutations, actions 合并,就是直接采用类似**解构**的方式,但是不是解构哈
getters: {
getNameToUpperCase() {},
...home.getters
}
mutations: {
changeName(state, payload) {},
...home.mutations
}
actions: {
asyncChangeName() {},
...home.actions
}
那么就会存在一种情况,如果函数名存在相同的话,就会执行多个相同函数名的函数(不是解构,因为解构的话,遇到相同的函数名就是覆盖操作了,但是并没有进行覆盖)
添加了 namespaced: true,就会进行模块名的拼接
就拿 mutations 中的一个来解释吧,getters / actions 类似。
// 伪代码
mutations: {
changeName(state, payload) {},
// 假设 home 模块中也存在 changeName 函数
// 当合并到 root 上时,就会进行拼接 '模块名/函数名'
'home/changeName': function(state, payload){}
}
这时,也许你就知道了,在触发模块中的 mutations 和 actions 时,是以这样的形式:
this.$store.commit('home/changeName', {name: 'james'})
this.$store.dispatch('home/asyncChangeName', {name: 'james'})
好奇内部是怎么实现的吗?
module 的源码阅读
针对 modules 属性中的模块,是都需要进行安装的,也就是注册到 root 模块上
function getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
// 根据 module.namespaced 拼接路径
// namespaced 为 true,返回拼接的字符串
// 为 false,就为 ''
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
export function installModule (store, rootState, path, module, hot) {
// 是否是根节点
const isRoot = !path.length
// 根据 namespaced 获取注册的 路径拼接字符串
const namespace = store._modules.getNamespace(path)
// 设置嵌套的 state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
// 拿到 path 的最后一个字符串
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 把 state 挂载到 父模块上
parentState[moduleName] = module.state
})
}
const local = module.context = makeLocalContext(store, namespace, path)
// 循环注册 mutations
module.forEachMutation((mutation, key) => {
// namespacedType: 注册的字符串
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
}
vuex 的辅助函数封装
在封装之前,我们要充分的了解到,辅助函数是一个函数,返回的是一个对象,对象的形式: {key: string; value: function}。
理解版:
- 利用 computed 的本质,接受一个函数
- 理解内部调用的机制(mapState 内部使用 this.$store.state 获取值,但是setup中不存在this,所以通过bind 改变 this,使内部的 this 指向我们传递的对象)
imoprt { computed } from 'vue'
import { mapState, useStore} from 'vuex'
export default useState(mapper) {
// 拿到 store 对象
const store = useStore()
// 使用 mapState 对传入的参数进行解析(数组对象都可)
// 返回值是一个对象(上面的 mapState 的结构解析)
const storeStateFun = mapState(mapper)
//定义一个对象
cosnt storeState = {}
//遍历对象,拿到key值,然后遍历key, 根据key,调用属性值函数
Object.keys(storeStateFun).forEach(funKey => {
//因为这是mapState解析后的函数,
//而mapState内部实现是通过,this.$store.state来获取
//然而setup中,又没有this,所以需要通过bind来改变this指向
//$route: store的实例对象
const fn = storeStateFun[funKey].bind({$store: store})
//composition API中computed需要传递一个函数
//把return的值,转化为一个ref
//然后对对象保存起来: 属性名: key, 属性值: ref对象
storeState[funKey] = computed(fn)
})
return storeState
}
完整版:
- 封装一致性(useState、useGetters、useMutations、useActions)
- 直接模块化传递
import { useStore, createNamespacedHelpers } from 'vuex'
import { computed } from 'vue'
// 封装一个工具函数
const commonFn = function(mapper, mapFn) {
const store = useStore()
const storeStateObj = mapFn(mapper)
const res = {}
Object.keys(storeStateObj).forEach(item => {
const fn = storeStateObj[item].bind({$store, store})
res[item] = computed(fn)
})
return res
}
// useState
const useState = (moduleName, mapper) => {
// moduleName: 存在使用的模块 mapState,不存在就是 root
let mapperFn = mapState
if(typeof moduleName === 'string' && moduleName.length > 0) {
//防止出现useState('', [])发生了
mapperFn = createNamespacedHelpers(moduleName).mapState
}
return commonFn(mapper, mapperFn)
}
// useGetters
const useGetters = (moduleName, mapper) => {
// moduleName: 存在使用的模块 mapGetters,不存在就是 root
let mapperFn = mapGetters
if(typeof moduleName === 'string' && moduleName.length > 0) {
//防止出现useState('', [])发生了
mapperFn = createNamespacedHelpers(moduleName).mapGetters
}
return commonFn(mapper, mapperFn)
}
// useMutations 和 useActions 等同
pinia
pinia 起始于 2019 年 11 月左右的一次实验,其目的是设计一个拥有组合式 API 的 Vue 状态管理库。
pinia 也被称为菠萝,是因为与西班牙语的菠萝的词很相似,由名得来。
pinia 的起源
pinia 诞生的原因: vuex 团队在 2019 年开发 pinia 就是为了探索 vuex 的下一次迭代会是什么,结果写着写着,最后发现 pinia 已经具备了 vuex5 (vuex 的下一次迭代) 中的大部分内容,所以决定使用 pinia 代替 vuex5 的出现。
来源于 coderwhy 老师的讲述,真假与否,我也不知道了~~~
pinia 的核心概念
其实理念跟 vuex 是一样的,只是去掉了一些概念(mutations 、modules)
pinia 的两个核心 api
// store/index.js
import { createPinia } from "pinia";
// 创建一个 pinia 实例
const pinia = createPinia();
export default pinia;
// main.js
app = createApp(App);
app.use(pinia);
app.mount(box);
import { defineStore } from 'pinia'
// 创建一个 store 实例
const useUserStore = defineStore('user', {
})
pinia 核心使用要点
/**
* =================================
* 定义和使用
* =================================
*/
import { defineStore } from 'pinia'
// 创建一个 store 实例
export const useUserStore = defineStore('user', {
state() {
return {
name: 'copyer'
}
}
})
// App.vue
import { useUserStore } from '@/store/useUserStore'
const userStore = useUserStore()
<div>{{ userStore.name }}</div>
/**
* =================================
* 响应式
* =================================
*/
// 这里的 name 不是响应式
const { name } = useUserStore()
// 两种方式
// 方式一:利用 vue 提供 api
import { toRefs } from 'vue'
const { name } = toRefs(useUserStore())
// 方式二:利用 pinia 提供 api
import { storeToRefs } from 'pinia'
const { name } = storeToRefs(useUserStore())
/**
* =================================
* 修改
* =================================
*/
// 直接修改(类似单个修改,写多次)
const userStore = useUserStore()
function btn() {
userStore.name = 'james'
}
// 直接修改(批量修改)
function btn() {
userStore.$patch({
name: 'james'
})
}
// 重置
function btn() {
userStore.$reset()
}
// 响应式对象替换
function btn() {
userStore.$state({
name: 'kobe',
age: 2
})
}
/**
* =================================
* state 的获取
* =================================
*/
export const useUserStore = defineStore("user", {
...
getters: {
// 方式一:getter 的第一个参数
// 方式二:this 直接拿取 实例 store
getNameToUpperCase(state) {
return state.name
}
},
});
/**
* =================================
* getter 之间的相互引用
* =================================
*/
// 通过 this 拿取store 的实例对象,直接拿取 getter
/**
* =================================
* 接受参数
* =================================
*/
export const useUserStore = defineStore("user", {
...
getters: {
// 返回一个函数,用于接受参数
getNameToUpperCase(state) {
return (str) => {
return state.name + '' + str
}
}
},
});
/**
* =================================
* 使用其他的 store
* =================================
*/
getNameToUpperCase(state) {
const countStore = useCountStore()
return (str) => {
return state.name + '' + countStore.count
}
}
其实没有什么可以讲的呢,就正常的函数使用即可。
export const useUserStore = defineStore("user", {
state() {
return {
name: "copyer",
};
},
actions: {
async changeName(name) {
this.name = name
}
}
});
pinia 的其他补充
辅助函数
pinia 的辅助函数就是为了对 options api 的支持。
- mapState 和 mapGetters 的使用基本一致,针对属性
- mapActions 针对函数
- mapStores 注册多个 store 到组件实例中,绑定在 this 上
import { useUserStore } from '@/store/useUserStore'
import { mapState, mapActions, mapStores, mapGetters } from 'pinia'
export default {
// mapState 与 mapGetters 使用相同
computed: {
// 其他计算属性
// useUserStore 有一个名为 `name` 的 state 属性 (三种写法)
...mapState(useUserStore, {
name: store => store.name + '' + '_name',
// 注意如果你想要使用 `this`,那你不能使用箭头函数
upperName(store) {
return store.name.toLocaleUpperCase()
},
name: 'name'
}),
// 注册多个 store 到组件中
...mapStores(useUserStore, useCountStore)
},
methods: {
// 其他方法属性
// useUserStore 有一个 action,分别是 `changeName`。
...mapActions(useUserStore, { changeName: 'changeName'})
},
created() {
setTimeout(() => {
this.changeName('james')
}, 2000)
}
}
定义 store 的两种方式
// options store
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({ name: 'copyer' }),
getters: {
getNameToUpperCase: (state) => state.name.toLocaleUpperCase()
},
actions: {
changeName(name) {
this.name = name
},
},
})
// setup store
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const name = ref('copyer')
const getNameToUpperCase = computed(() => {
return state.name.toLocaleUpperCase()
})
function changeName(name) {
name.value = name
}
return {name, getNameToUpperCase, changeName}
})
pinia 对比 vuex 的优势
- 最大的一个特点就是: vuex 是层级化,pinia 是扁平化。而扁平化的数据更加利用操作。
- 更加少的 api, 更加优雅的写法,也提供了多种写法
// 更加少的 api
vuex 需要通过 commit dispatch 触发函数, pinia 直接触发函数
去掉了 mutations 和 modules 概念,更加好理解
// 更加优雅的写法: 不在需要深层次 .state .getters 了
vuex 的写法
<div> {{ $store.state.name }}</div>
<div> {{ $store.state.age }}</div>
<div> {{ $store.getters.getNameToUpperCase }}</div>
pinia 的写法
<div> {{ userStore.name }}</div>
<div> {{ userStore.age }}</div>
<div> {{ userStore.getNameToUpperCase }}</div>
// 多种写法
options store
setup store
- 对 typescript 更加的友好,提示也更加的智能。
- 去掉了 mutations,也能使用 devtools 对数据进行监控。
结语
总结完成了,存在有误的地方,请多多指教~~~