写这篇文章是因为很久没写前端业务了,导致一些很简单的概念都慢慢忘记,于是想用文字的方式梳理一遍,加深印象。岁月不饶人啊。
现代前端框架都是使用组件化机制来搭建整个项目,每个组件内部有自己的数据和模板。
但是总有些数据是需要共享的,比如当前登录的用户名、权限等数据,如果都在组件内部传递,会变得非常混乱。
因此,需要一个东西来集中式存储管理应用的公用状态,这就是前端状态管理。
本文以前端状态管理库 Vuex 为例,来讲清楚前端状态管理是如何实现的。
Vuex 的使用
先来看下 Vuex 的使用
import { createStore } from 'vuex'
const store = createStore({
// 注意state是一个函数
state () {
return {
count: 666
}
},
mutations: {
add (state) {
state.count++
}
}
})
我们使用 createStore 来创建一个数据存储,称之为 store。
store 内部除了数据,还需要一个 mutation 配置去修改数据,mutation 内部的函数会把 state 作为参数,操作 state.count 就可以完成数据的修改。
现在,在 Vue 的组件系统之外,多了一个数据源,里面只有一个变量 count,并且有一个方法可以累加这个 count。
然后,在 Vue 中注册这个数据源,在项目入口文件 src/main.js 中,使用 app.use(store) 进行注册,这样 Vue 和 Vuex 就连接上了。
const app = createApp(App)
import store from './store
app.use(store).use(router).mount('#app')
在组件中使用Vuex定义的state:
<template>
<div @click="add">
{{count}}
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
let store = useStore()
let count = computed(()=>store.state.count)
function add(){
store.commit('add')
}
</script>
count 不是使用 ref 直接定义,而是使用计算属性返回了 store.state.count,也就是刚才在 src/store/index.js 中定义的 count。
add 函数是用来修改数据,修改数据可以像 store.state.count += 1 这样写吗?
其实是可以的,但是如果这样做了,Vuex 不能够记录每一次 state 的变化记录,影响我们的调试。
当 Vuex 开启严格模式的时候,直接修改 state 会抛出错误,所以官方建议我们开启严格模式strict: true,所有的 state 变更都在 Vuex 内部进行,所以我们要使用 store.commit('add') 去触发 Vuex 中的 mutation 去修改数据。
现在就产生了一个问题:什么时候的数据用 Vuex 管理,什么时候数据要放在组件内部使用 ref 管理呢?
对于一个数据,如果只是组件内部使用就是用 ref 管理;
如果我们需要跨组件,跨页面共享的时候,我们就需要把数据从 Vue 的组件内部抽离出来,放在 Vuex 中去管理。
知道了 Vuex 的使用,接下来我们实现一个迷你的 Vuex, 明白Vuex 的大致原理。
手写迷你 Vuex
实现思路也比较简单,其实就是创建一个变量 store 用来存储数据,然后把这个 store 的数据包转成响应式的数据,并且提供给 Vue 组件使用,这样当数据变化后,组件也随之变化。
在 Vue 中有 provide/inject 这两个函数专门用来做数据共享,provide 注册了数据后,所有的子组件都可以通过 inject 获取数据。
import { inject, reactive } from 'vue'
const STORE_KEY = '__store__'
function useStore() {
return inject(STORE_KEY)
}
function createStore(options) {
return new Store(options)
}
class Store {
constructor(options) {
this._state = reactive({
data: options.state()
})
this._mutations = options.mutations
}
}
export { createStore, useStore }
上面的代码导出了两个函数createStore, useStore。
createStore 去创建 Store 的实例。
useStore可以在任意组件的 setup 函数内去获取 store 的实例。
在项目入口文件 src/main.js 中使用 app.use(store) 注册。
为了让 useStore 能正常工作,下面的代码中,我们需要给 store 新增一个 install 方法,这个方法会在 app.use 函数内部执行。
我们通过 app.provide 函数注册 store 给全局的组件使用。
class Store {
// main.js入口处app.use(store)的时候,会执行这个函数
install(app) {
app.provide(STORE_KEY, this)
}
}
补全其他代码,完整代码如下:
import { inject, reactive } from 'vue'
const STORE_KEY = '__store__'
function useStore() {
return inject(STORE_KEY)
}
function createStore(options) {
return new Store(options)
}
class Store {
constructor(options) {
this.$options = options
// _state私有变量
this._state = reactive({
data: options.state
})
this._mutations = options.mutations
}
// 提供给外部使用
get state() {
return this._state.data
}
commit = (type, payload) => {
const entry = this._mutations[type]
entry && entry(this.state, payload)
}
install(app) {
app.provide(STORE_KEY, this)
}
}
export { createStore, useStore }
这样借助 Vue 的插件机制和 reactive 响应式功能,这里只用 30 行代码,就实现了一个最迷你的数据管理工具Vuex。
Pinia VS Vuex
Pinia 和 Vuex 一样都是是 vue 的全局状态管理器,其实 Pinia 就是 Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字。
首先看下 Pinia 的使用:
import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {
state: () => {
return {
msg: "hello pinia",
};
},
getters: {},
actions: {},
});
修改 state 里面的数据有三种方式:
- 直接修改
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
piniaStoreA.piniaMsg = 'hello juejin'
$patch
使用$patch方法可以修改多个state中的值
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
piniaStoreA.$patch({
piniaMsg: 'hello juejin',
name: 'daming'
})
// $patch还可以使用函数的方式进行修改状态
cartStore.$patch((state) => {
state.name = 'daming'
state.piniaMsg = 'hello juejin'
})
- 在
actions中进行修改
不同于Vuex的是,Pinia去掉了 mutations。
import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {
state: () => {
return {
piniaMsg: "hello pinia",
name: "xiao yue",
};
},
actions: {
setName(data) {
this.name = data;
},
},
});
// 使用
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
piniaStoreA.setName('daming')
这也是比较推荐的一种修改状态的方式,就像上面说的,这样可以实现整个数据流程都在状态管理器内部,便于管理。
Pinia 解构
当我们组件中需要用到 state 中多个参数时,使用解构的方式取值往往是很方便的,但是传统的ES6解构会使 state 失去响应式,比如组件App.vue,我们先解构取得 name 值,然后再去改变name值。
<template>
<div>{{ name }}</div>
</template>
<script setup>
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
let { piniaMsg, name } = piniaStoreA
piniaStoreA.$patch({
name: 'daming'
})
</script>
发现浏览器上显示的值并没有更新。为了解决这个问题,Pinia提供了一个结构方法 storeToRefs。
import { storeA } from '@/piniaStore/storeA'
import { storeToRefs } from 'pinia'
let piniaStoreA = storeA()
let { piniaMsg, name } = storeToRefs(piniaStoreA)
modules
如果项目比较大,使用单一状态库,项目的状态库就会集中到一个大对象上,显得十分臃肿难以维护。所以Vuex就允许我们将其分割成模块(modules),每个模块都拥有自己state,mutations,actions...。
而Pinia每个状态库本身就是一个模块,它没有modules,如果想使用多个store,直接定义多个store传入不同的id即可,如:
import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {...});
export const storeB = defineStore("storeB", {...});
export const storeC = defineStore("storeB", {...});
总结
本文首先回顾了 Vuex 的使用,然后按照使用方式手写了一个简单的迷你版的 Vuex。
核心原理就是在全局维护一个响应式的数据包 store,当组件使用某个数据时,这个数据通过依赖收集收集到这个组件,这样当数据更新时组件就会自动更新。最后通过provide/inject来引用这个数据包。
文章的最后简单介绍了下 Pinia 的使用,它的使用非常简单,它没有 Vuex 中的mutations ,所有的数据操作通过commit来修改。另外,它也没有 modules,如果想使用多个store,直接定义通过defineStore多个store传入不同的id即可。