vuex和pinia
vuex
基本使用
//index.js
import { createStore } from './gvuex.js'
const store = createStore({
// 定义store的状态
state() {
return {
count: 1
}
},
// 定义获取state状态数据的计算属性getters,以下的值都被认为是state的派生值
getters: {
double(state) {
return state.count * 2
}
},
// 定义修改state状态数据的方法mutations
mutations: {
add(state) {
state.count++
}
},
// 定义异步操作的方法actions
actions: {
asyncAdd({ commit }) {
setTimeout(() => {
commit('add')
}, 1000)
}
}
})
// App.vue
<script setup>
import { useStore } from '../store/gvuex'
import { computed } from 'vue'
let store = useStore();
let count = computed(()=>{ store.state.count })
let double = computed(()=>{ store.getters.double })
function add() {
store.commit('add')
}
function asyncAdd() {
store.dispatch('asyncAdd')
}
</script>
<template>
<div class="">
{{ count }} * 2 = {{ double }}
<button @click="add">add</button>
<button @click="asyncAdd">async add</button>
</div>
</template>
<style scoped>
</style>
知道了vuex的用法,你会不会发出以下疑问:
1、为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?
2、为什么不可以直接对state存储的状态进行修改,只能通过调用mutation函数的方式修改呢?
3、为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?
4、createStore()和useStore()到底发生了什么?
那么下面就来一一解密吧。
vue里注册全局组件
import { createApp } from 'vue'
import store from './store'
import App from './App.vue'
const app = createApp(App)
app
.use(store)
.mount('#app')
app.use() 用来安装插件,接受一个参数,通常是插件对象,该对象必须暴露一个install方法,调用app.use()时,会自动执行install()方法。
解析Store类里的流程
import { reactive, inject } from 'vue'
// 定义了一个全局的 key,用于在 Vue 组件中通过 inject API 访问 store 对象
const STORE_KEY = '__store__'
// 用于获取当前组件的 store 对象
function useStore() {
return inject(STORE_KEY)
}
// Store 类,用于管理应用程序状态
class Store {
// 构造函数,接收一个包含 state、mutations、actions 和 getters 函数的对象 options,然后将它们保存到实例属性中
constructor(options) {
this.$options = options;
// 使用 Vue.js 的 reactive API 将 state 数据转换为响应式对象,并保存到实例属性 _state 中
this._state = reactive({
data: options.state()
})
// 将 mutations 和 actions 函数保存到实例属性中
this._mutations = options.mutations
this._actions = options.actions;
// 初始化 getters 属性为空对象
this.getters = {};
// 遍历所有的 getters 函数,将其封装成 computed 属性并保存到实例属性 getters 中
Object.keys(options.getters).forEach(name => {
const fn = options.getters(name);
this.getters[name] = computed(() => fn(this.state));
})
}
// 用于获取当前状态数据
get state() {
return this._state.data
}
// 获取mutation内定义的函数并执行
commit = (type, payload) => {
const entry = this._mutations[type]
entry && entry(this.state, payload)
}
// 获取actions内定义的函数并返回函数执行结果
// 简略版dispatch
dispatch = (type, payload) => {
const entry = this._actions[type];
return entry && entry(this, payload)
}
// 将当前 store 实例注册到 Vue.js 应用程序中
install(app) {
app.provide(STORE_KEY, this)
}
}
// 创建一个新的 Store 实例并返回
function createStore(options) {
return new Store(options);
}
// 导出 createStore 和 useStore 函数,用于在 Vue.js 应用程序中管理状态
export {
createStore,
useStore
}
是不是很惊讶于vuex的底层实现就短短几十行代码就实现了,嘿嘿那是因为从vue里引入了reactive、inject和computed,并且对很大一部分的源码进行了省略,dispatch和commit远比这复杂多了,有兴趣去了解reactive的实现可以去看我另一篇文章学VUE源码之手写min版响应式原型 - 掘金 (juejin.cn)[1],下面解答上面抛出的问题吧。
解答
问题一:为什么要store.commit('add')才能触发事件执行呢? 可不可以进行直接调用mutation函数进行操作呢?
解答:store类里根本没有mutation方法,只能通过调用commit方法来执行mutation里的函数列表。
问题二:为什么不可以直接对state存储的状态进行修改,只能通过调用函数的方式修改呢?
解答:Vuex 通过强制限制对 store 的修改方式来确保状态的可追踪性。只有通过 mutation 函数才能修改 store 中的状态,这样可以轻松地跟踪状态的变化,也可以避免无意中从不同的组件中直接修改 store 导致的代码难以维护和调试的问题。
问题三:为什么存在异步调用的函数需要store.dispatch('asyncAdd')函数才能完成呢?可以直接调用store.commit('asyncAdd')嘛?如果不可以,为什么呢?
解答:实际上dispatch方法和commit方法远不止这么简单,下面先贴出部分vuex的关于这两个方法的源码部分
Store.prototype.dispatch = function dispatch (_type, _payload) {
var this$1$1 = this;
// check object-style dispatch
var ref = unifyObjectStyle(_type, _payload);
var type = ref.type;
var payload = ref.payload;
var action = { type: type, payload: payload };
var entry = this._actions[type];
if (!entry) {
if ((process.env.NODE_ENV !== 'production')) {
console.error(("[vuex] unknown action type: " + type));
}
return
}
try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(function (sub) { return sub.before; })
.forEach(function (sub) { return sub.before(action, this$1$1.state); });
} catch (e) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn("[vuex] error in before action subscribers: ");
console.error(e);
}
}
var result = entry.length > 1
? Promise.all(entry.map(function (handler) { return handler(payload); }))
: entry[0](payload);
return new Promise(function (resolve, reject) {
result.then(function (res) {
try {
this$1$1._actionSubscribers
.filter(function (sub) { return sub.after; })
.forEach(function (sub) { return sub.after(action, this$1$1.state); });
} catch (e) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn("[vuex] error in after action subscribers: ");
console.error(e);
}
}
resolve(res);
}, function (error) {
try {
this$1$1._actionSubscribers
.filter(function (sub) { return sub.error; })
.forEach(function (sub) { return sub.error(action, this$1$1.state, error); });
} catch (e) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn("[vuex] error in error action subscribers: ");
console.error(e);
}
}
reject(error);
});
})
};
Store.prototype.commit = function commit (_type, _payload, _options) {
var this$1$1 = this;
// check object-style commit
var ref = unifyObjectStyle(_type, _payload, _options);
var type = ref.type;
var payload = ref.payload;
var options = ref.options;
var mutation = { type: type, payload: payload };
var entry = this._mutations[type];
if (!entry) {
if ((process.env.NODE_ENV !== 'production')) {
console.error(("[vuex] unknown mutation type: " + type));
}
return
}
this._withCommit(function () {
entry.forEach(function commitIterator (handler) {
handler(payload);
});
});
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(function (sub) { return sub(mutation, this$1$1.state); });
if (
(process.env.NODE_ENV !== 'production') &&
options && options.silent
) {
console.warn(
"[vuex] mutation type: " + type + ". Silent option has been removed. " +
'Use the filter functionality in the vue-devtools'
);
}
};
源码里我们能看到,dispatch方法返回的是一个Promise对象,而commit方法没有返回值,完全进行的是同步代码的操作,虽然返回值可能适用的场景不多,但是这样设计的主要目的还是为了确保状态的可追踪性
问题四: createStore()和useStore()到底发生了什么?
当我们去调用 createStore()函数,其构造函数就会接收一个包含 state、mutations、actions 和 getters 函数的对象 options, 然后将它们保存到实例属性中,此时state中的值都会转换为响应式对象,同时遍历所有的getters函数,将其封装成computed属性并保存到实例属性getters中,而在main.js里调用了app.use(), install方法自动执行,将将当前 store 实例注册到 Vue.js 应用程序中,只需要调用useStore()就可以拿到全局状态管理的store实例了,可以靠inject和provide实现全局共享。
pinia
官网:https://pinia.vuejs.org/zh/
为什么推荐使用pinia
Vue2和Vue3都支持
pinia中只有state、getter、action,抛弃了Vuex中的Mutation,减少工作量
pinia中action支持同步和异步,Vuex中的action只支持异步
良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
体积非常小,只有1KB左右。
pinia支持插件来扩展自身功能。
支持服务端渲染。
核心概念
Pinia 的四个核心概念:
store:承载全局状态。
state:state 是 store 的数据(data),相当于组件中的 data
getters:store 中的计算属性,相当于组件中的计算属性(computed)
actions:store 中的方法,相当于组件中的方法(methods)
基础用法
创建一个store
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => {
return { count: 0 }
},
// 也可以这样定义
// state: () => ({ count: 0 })
actions: {
increment() {
this.count++
},
},
})
组件里使用:
<template>
<!-- 直接从 store 中访问 state -->
<div>Current Count: {{ counter.count }}</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
counter.count++
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 或使用 action 代替
counter.increment()
</script>
持久化
Pinia 的数据是临时存储的,当页面刷新时,数据就会失效。而对于有些场景,我是需要持久化到本地存储的,简单一些的场景可以直接使用 LocalStorage 或者 SessionStorage API。
不过更推荐的方式是持久化插件,比如 pinia-plugin-persistedstate
。
安装插件
npm install pinia-plugin-persistedstate
注册插件
在创建 pinia 实例时,注册插件:
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia
使用插件
在需要持久化的store中, 开启持久化模式即可。
import { defineStore } from "pinia"
export const useXXXStore = defineStore("xxx", {
persist: true,
...
})
默认情况下,持久化的数据使用的是 localStorage,其 key则使用的是 store 的 id。存储的数据是 state 的数据。
另外,也可以针对 persist 进行单独设置:
import { defineStore } from "pinia"
export const useXXXStore = defineStore("xxx", {
persist: {
key: "XXX",
storage: sessionStorage,
},
...
})
使用体验
赋值、获取值的时候,pinia比vuex简单
vuex赋值的时候有俩种方式:
1、通过commit()提交store里定义的Mutation,Mutation方法里第一个参数是state,第二个参数才是需要赋值的值
要给state里定义的routes赋值,需要用state.routes
SET_ROUTES: (state, routes) => {
state.routes = routes
}
2、通过dispatch调用定义的actions,在actions里用commit()提交Mutation,改变state里的值
// 退出系统
logOut({ commit }) {
return new Promise((resolve, reject) => {
logout(this.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_PERMISSIONS', [])
removeToken()
helper.setToken()
resolve()
}).catch(error => {
reject(error)
})
})
}
pinia只有action,将多余的Mutation去掉了,而且在action里调用state定义的值和其他action方法,都是直接用this
// 登录退出
loginOut() {
return new Promise((resolve, reject) => {
logout().then(() => {
this.setName('')
this.setAvatar('')
resolve()
}).catch(error => {
reject(error)
})
})
}