每天对自己多问几个为什么,总是有着想象不到的收获。
一个菜鸟小白的成长之路(copyer)
前言:
这篇博客主要介绍了vuex4的基本使用,从创建一个store对象,然后再store内部的属性进行一一的解释(就是vuex的五大和核心:state, getters,mutations,actions, modules),解释主要从类型,参数等等方面的解释,还是比较的详细。内容有点长,可以收藏起来,慢慢看哟(嘻嘻)
vuex介绍
Vuex 是一个专为 Vue.js 应用程序开发的
状态管理模式 + 库
。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
具体就不描述了,懂得都懂,不懂的,我也说不清(哈哈哈)
vuex4安装
npm install vuex@next
//现在默认是vuex3,所以加上next安装下一个版本
//package.json文件中,查看版本
"dependencies": {
"vuex": "^4.0.2"
},
vuex创建一个store对象
在vuex4中提供了一个方法,
createStore
来创建一个store对象
如果不想看泛型分析的,可以直接跳过引用部分
对
createStore
的类型进行分析// vuex/types/index.d.ts export function createStore<S>(options: StoreOptions<S>): Store<S>
分析:
createStore
接收一个泛型, 泛型的名称为
S
接收一个参数,参数为对象,对象的类型为StoreOptions
返回一个
Store
实例
分析参数
export interface StoreOptions<S> { state?: S | (() => S); //对象或则函数的形式,返回对象 getters?: GetterTree<S, S>; actions?: ActionTree<S, S>; mutations?: MutationTree<S>; modules?: ModuleTree<S>; plugins?: Plugin<S>[]; strict?: boolean; devtools?: boolean; }
根据上面的接口,可以看出
泛型S
就是 对 state的类型进行限定
分析返回值
Store 是一个泛型类
// vuex/dist/vuex.global.js function createStore (options) { return new Store(options) }
返回的就是Store类的一个实例对象
创建一个
store
实例对象,并且其类型定义为
IRootState
import { createStore } from 'vuex'
interface IRootState {
count: number
}
const store = createStore<IRootState>({ //定义state的类型限定
})
export default store
注册到项目的APP中
import { createApp } from 'vue'
import store from '@/store'
const app = createApp(App)
app.use(store)
app.mount('#app')
这样就可以直接在各个组件中使用store了。
vuex核心模块之state
存放数据的地方(数据仓库),state可以是一个函数,也可以是一个对象
const store = createStore<IRootState>({ //定义state的类型限定
state() { //创建的时候,已经给state的类型进行了限定
return {
count: 0
}
}
})
上面,我们已经定义好了一个state数据,那么我们就可以在组件中展示出来了。分类两种形式:
第一种形式:
//直接在组件中使用$store
<h1>{{$store.state.count}}</h1>
注意: 上面的这种情况有点特殊,如果是我们手动安装vuex的话,ts是对
$store
不认识的,就会报警告,但是不影响项目的运行流程。但是对于大多数人,肯定还是接受不了波浪线的警告。 情景大致如下:
那应该如何解决呢,使ts认识
$store
?
在vue3创建的项目中,有一个文件
shims-vue.d.ts
//使用.d.ts来申明一下$store, 就可解决了
declare let $store: any
第二种形式:
借用vue3中提供的computed这个API,然后再进行展示(这里就不会存在上面的警告文件)
还需要借助一个函数
useStore
, 这个是vuex内部提供的,因为在setup中是不能使用this的,就不能像vue2中通过
this.$store
的形式。
import { defineComponent, computed} from 'vue'
import { useStore } from 'vuex'
export default defineComponent({
name: 'App',
setup() {
const store = useStore()
const count = computed(() => store.state.count) //拿到count的值
return {
count
}
}
})
//在template中展示
<h1>{{count}}</h1>
上面两种形式的效果是一样的。(选哪种,看你随意)
- 第一种在template中字段太长,代码不优雅
- 第二种在需要借助两个函数,代码量增加
vuex核心模块之getters
getters的作用就是对state进行变形后,然后进行返回展示
这里的count实例不好演示,我就列举了另外的数据进行演示(更好的体现出来)
interface IRootState {
coder: any[]
}
const store = createStore<IRootState>({
state() {
return {
coder: [
{name: '前端人员', number: 5},
{name: '后端人员', number: 15},
{name: '测试人员', number: 3},
{name: '产品', number: 2}
]
}
},
getters: {
coderAllCount(state) {
return state.coder.reduce((prev, item) => {
return prev + item.number
}, 0)
}
}
})
export default store
代码分析:这里的state的数据是
coder
,是一个数据,如果现在在界面中
展示总的人数
,getters就是很好的选择(其实还有一种情况就是,组件直接拿到state中的值,进行计算展示,那么想象一下,如果很多地方也需要总人数,那么就要写很多遍),在getters中直接写好,这样就可以在多处的地方使用了,而不需要重复的写相同的代码。
在组件中展示,跟state的两种形式是一样的
//形式一
<h1>{{$store.getters.coderAllCount}}</h1>
//形式二
const coderAllCount = computed(() => store.getters.coderAllCount)
对
getters
的参数分析
在上面的实例中,已经可以看见,其中的一个参数就是state,这个state就是store中的state。分析:
ts对getters的类型分析,如果不想看,可以直接跳过
从上面可以找到,createStore接收一个参数,为对象,对象的类型为
StoreOptions
,其中有一个属性就是
getters
,定义如下getters?: GetterTree<S, S>; //泛型接口 //GetterTree interface GetterTree<S, R> { [key: string]: Getter<S, R>; //泛型接口(知道方法名为字符串) } //Getter type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;
这里考虑到了模块的存在
在上面的分析的过程中,知道了
接收四个参数
,分别是:
state、getters、rootState、rootGetters
,
当然这里考虑到了模块的存在,所以这个在下面讲解模块的时候在讨论。
这里,我们是跟Store,所以也解释了为什么
GetterTree<S, S>
传递了两个相同的泛型,并且这里的
state跟rootState是全等的, getters和rootGetters也是全等的
个人看法: getters中的属性虽然是个函数,请把它看成是属性。(因为在使用的时候,不需要
()
)
vuex核心模块之mutations
1、mutations是干什么的?
mutations是更改vuex中的store的状态唯一方法。
2、怎么触发mutations中的方法?
通过
createStore
创建出来的实例对象中的
commit属性
,该属性是一个方法,接收的参数可以是
字符串和对象
的形式(
payload:提交载荷
)
3、mutations中的函数可以是异步函数吗?
不可以,mutation中的必须是同步代码(重要的原则)。比如说:在mutation的日志的时候,
devtools 都需要捕捉到前一状态和后一状态的快照
,然而当时异步函数的时候,
devtools根本不知道什么执行
,从而也没法捕捉,那么
状态就是一个可不追踪状态
参数分析:
ts 对 mutations 的参数类型分析,如果不想看,可以直接跳过
从上面可以找到,createStore接收一个参数,为对象,对象的类型为
StoreOptions
,其中有一个属性就是
mutations
,定义如下:mutations?: MutationTree<S>; //泛型接口 export interface MutationTree<S> { [key: string]: Mutation<S>; //泛型接口(知道方法名为字符串) } //Mutation export type Mutation<S> = (state: S, payload?: any) => any;
从上面的类型分析,我们可以看出 mutation中的方法
接收两个参数
:
state 、 payload(可选)
mutation只能修改当前模块中的state
代码示例
// store/index.ts
interface IRootState {
count: number
}
const store = createStore<IRootState>({
state() {
return {
count: 0
}
},
mutations: {
increment(state) { //这里的state的类型可以自动推导为IRootState
state.count += 1
}
}
})
在组件中触发(例如:点击事件)
//组件中
import { useStore } from 'vuex'
export default defineComponent({
name: 'App',
setup() {
const store = useStore()
const btn = () => {
store.commit('increment') // increment对应的就是mutations中的方法名
}
return {
btn
}
}
})
在上面,唯一容易出错的感觉的单词容易写错,,然后产出bug。如果想解决上面的问题,那就使用:
常量代替Mutation事件
//mutation-types.js
export const INCREMENT = 'INCREMENT' // 加
// store/index.ts
import { INCREMENT } from './mutation-types'
mutations: {
[INCREMENT] (state) {
state += 1
}
}
//组件中
import { INCREMENT } from '@/store/mutation-types'
store.commit(INCREMENT) //使用导出的常量
这样,就可以减少错误,尽管单词写错了。而且这样还有个好处就是,可以一眼分析出:执行了哪些mutations,而不会不停的mutations中, 折叠寻找代码。
提交载荷(payload)
这里主要是两种形式:
字符串
和
对象
推荐使用:
对象形式
代码演示:
const store = createStore<IRootState>({
state() {
return {
count: 0
}
},
mutations: {
increment(state, payload) { //这里的state的类型可以自动推导为IRootState
state.count += payload.count
}
}
})
//字符串形式
const btn = () => {
store.commit('increment', {count: 10})
//或则
store.commit(INCREMENT, {count: 10})
}
//对象的形式
const btn = () => {
store.commit({
type: 'increment',
count: 10
})
//或则
store.commit({
type: INCREMENT,
count: 10
})
}
//这里对象的形式: payload是指向传递的整个对象
vuex核心模块之actions
1、action可以直接修改state的状态吗?
不可以,store中修改state的值,只有mutation,所以action提交的是mutation,让mutation来进行修改
2、怎么触发actions中的方法?
通过
createStore
创建出来的实例对象中的
dispatch属性
,该属性是一个方法,接收的参数可以是
字符串和对象
的形式(
payload:提交载荷
)
3、action中可以写异步函数吗?
可以,action就是专门用来处理异步的,当异步执行完成之后,触发mutation,从而修改state的状态,让state变成可追踪的。
参数分析
ts 对 action的参数类型分析,如果不想看,可以直接跳过
(推荐看下,因为这个参数比较复杂)从上面可以找到,createStore接收一个参数,为对象,对象的类型为
StoreOptions
,其中有一个属性就是
actions
,类型定义如下:actions?: ActionTree<S, S>; export interface ActionTree<S, R> { [key: string]: Action<S, R>; } export type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>; //联合类型 ActionHandler | ActionObject export type ActionHandler<S, R> = (this: Store<R>, injectee: ActionContext<S, R>, payload?: any) => any; //第一个参数为this (不用传递) //第二个参数 注入一个context, 为一个对象 //第三个参数 提交负载(payload) export interface ActionObject<S, R> { //针对于模块中的action,触发根节点中的store root?: boolean; handler: ActionHandler<S, R>; } //注册context的类型 export interface ActionContext<S, R> { dispatch: Dispatch; commit: Commit; state: S; getters: any; rootState: R; rootGetters: any; }
从上面的类型大致,可以分析出:
actions中的方法,一般接收
两个参数
,
第二个参数为可选参数
参数一: context(必选参数)
context是一个对象
,包含以下属性:commit, dispatch, state, rootState, getters, rootGetters
- commit:触发mutations中的方法,修改state
- dispatch: 触发actions中的方法
-
state: 拿到
当前模块
中的state中的值 -
getters: 拿到
当前模块
中的getters中的值 -
rootState: 拿到
根节点
中的state值 -
rootGetters: 拿到
根节点
中的getters中的值
//参数一的两种形式的写法
actions: {
//写法一: 直接写context对象
increment(context) {},
//写法二: 解构对象,只列出我们所需要的属性名
decrement({commit, dispatch})
}
参数二: payload(可选参数)
payload的存在在于,我们是否给action分发的是否,是否携带参数。
// store/index.ts
const store = createStore<IRootState>({
state() {
return {
count: 0
}
},
mutations: {
increment(state, payload) { //这里的state的类型可以自动推导为IRootState
state.count += payload.count
}
},
actions: {
increment({ commit }, payload) {
setTimeout(() => {
commit({
type: 'increment',
count: payload.count
})
}, 1000)
}
}
})
情况一: 没有携带参数
const store = useStore()
cosnt btn = () => {
store.dispatch('increment') // 在这里就没有携带参数,所以payload是不存在的
}
情况二: 携带参数
const store = useStore()
//两种形式: 直接传递参数形式 和 对象的形式
cosnt btn = () => {
store.dispatch('increment', {count: 100}) // 在这里就有携带参数,所以payload是就是一个对象
}
//payload ==> {count: 100}
cosnt btn = () => {
store.dispatch({
type: 'increment',
count: 100
})
}
//payload ==> {type: 'increment', count: 100} 对象的形式有点差异
vuex核心模块之module
1、为什么要使用module?
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。所以,就需要对模块进行划分,这样思路也比较清晰,也利于团队分工开发。
类型分析:
ts对模块类型的进行分析,如果不想看,可以直接跳过
定义一个模块:
const countModule = {}
那么
countModule
的类型是什么呢?//这是vuex4对模块的类型定义 export interface Module<S, R> { namespaced?: boolean; state?: S | (() => S); getters?: GetterTree<S, R>; actions?: ActionTree<S, R>; mutations?: MutationTree<S>; modules?: ModuleTree<R>; }
从上面可以看出,跟
createStore
创建的基本实例是一样的,就是多了一个
namespaced
这属性(后面会说到)所以定义一个模块的类型确定
import { Module } from 'vuex' const countModule: Module<S, R> = {}
S
: 是模块中state的类型限定
R
: 是根store中的state的类型限定
基本使用:(计数模块为例)
准备工作:接口定义
interface ICountState { //count模块中state对象限定
count: number
}
interface IRootState { //根store中state的类型限定
count: number
}
第一步:定义一个countModule模块
import { Module } from 'vuex'
//type: Module<S, R>
//接收两个泛型,第一个S 为当前模块的state的类型, 第二个R: 就是跟节点的state类型
const countModule: Module<ICountState, IRootState> = {
state() {
return {
count: 0
}
},
getters: {},
mutations: {
increment(state) {
console.log('模块store中的increment方法')
state.count += 1
}
},
actions: {}
}
export default countModule
第二步:在根store中注册计数模块
import { createStore } from 'vuex'
import countModule from './count/count'
const store = createStore<IRootState>({
state() {
return {
count: 0
}
},
mutations: {
increment(state) {
console.log('根store中的increment方法')
state.count += 1
}
}
modules: {
countModule
}
})
代码分析:
定义了一个计数模块,该模块中的state中有一个count属性,mutations中有个increment方法;根store中state也有个count属性,mutations中有个increment方法;
(这里只测试mutations,因为getters和actions跟mutations的情况是一样的)
正题开始:
1、根store中的state和模块中的state的区分
疑问:怎么渲染
根store中的count
和
计数模块中的count
?
<div>{{$store.state.rootCount}}</div> //渲染根store (这个应该没有疑问吧)
<div>{{$store.state.countModule.count}}</div> //正确渲染count模块中的count (有疑问吗?)
//为什么不是下面的写法呢?
<div>{{$store.countModule.state.count}}</div> //消耗的性能更大,源码的内部数据结构不是这样设计
解答:state中的合并操作
vuex中
合并操作
的数据结构:
//简单大致如下:
const store = createStore({
state() {
return {
rootCount: 0,
countModule: { //这里直接添加module的state
count: 1
}
}
}
})
2、根store中的mutation和模块中的mutation的区分
疑问: mutations中的疑惑
- 如何根store中的mutations?
- 如何使用模块中的mutations?
- 如果根store中的方法与模块中的mutations中的方法一致,是怎么处理的
测试案例:
import { useStore } from 'vuex'
export default defineComponent({
setup() {
const store = useStore()
const btn = () => {
store.commit('increment')
}
}
})
我们通过
store中commit
方法来触发了
mutations中的increment
,但是由于
根store中内部mutations中有increment方法
,而
模块中的mutations中也有increments方法
,所以当点击会发生的效果呢?
初始状态的现象:
当点击按钮之后的效果
通过上面的测试,我们知道,
commit中同一个名字的increment都会被触发
,无论是模块中的还是根store中的。
那么根store中的mutation和模块中的mutation是怎么合并呢?
//会集中到根store中的mutations中
const store = createStore({
mutations: {
increment(){}, //根store
increment(){}, //模块中
}
})
比如像上面的实例代码(我们可以想象是这样理解)。
getters和actions的效果是一样的。
就想修改触发模块中的increment方法,而不想触发根store中的increment;或则说,就想触发根store中的increment方法,而不想触发模块中的increment,那么我们应该怎么处理呢?
在vuex4中,想要处理上面的类似效果,就需要使用
命名空间
命名空间的使用
在分析创建模块的类型的时候,里面有个属性
namespaced
,为一个boolean值,就是提醒我们是否需要开启命名空间。
-
默认情况下,模块内的action额mutations仍然是注册在全局的命名空间中的:
- 这样使得多个模块能够对同一个action或mutation作出响应
- getters同样也默认注册在全局命名空间
-
如果我们希望模块具有跟高的封装度和复用性,可以添加
namespaced: true
的方式使其成为待命名空间的模块:- 当模块被注册之后,它的所有getters,actions和mutation都会自动根据模块注册的路径调整命名。
对模块中的各个属性方法的访问:
//state (当然原来也是这么访问的)
store.state.countModule.count
//getters
store.getters['countModule/coderAllCount']
//mutations
store.commit('countModule/increment')
//actions
store.dispatch('countModule/incrementAction')
模块内部触发根store中的mutation
有不有 这种情况, 模块内部的action异步拿到数据后,可以想修改根store中的mutation呢? 应该会存在这种情况吧。那么这种情况该怎么处理呢?
在上面分析action的类型的时候,有一个类型是专门针对于模块的
export interface ActionObject<S, R> { //针对于模块中的action,触发根节点中的store
root?: boolean;
handler: ActionHandler<S, R>;
}
这里有个root属性,就是用来告诉
是否用来修改根store中的mutation
actions: {
incrementAction({commit}, payload) {
setTimeout(() => {
//默认情况下,就是触发模块内部mutation中的increment
commit('increment', payload)
//加上root:true, 那么这时候触发的就是根store中的increment了
commit('increment', payload, {root: true})
}, 1000)
}
}
辅助函数
当然还可以在vuex使用辅助函数,可以看我的另外一篇博客哟,在vue3中使用,对辅助函数的封装
总结:
vuex的基本用法大致就是这样吧,跟vuex3的区别也不是很大,以前学了vue2, 还是可以很容易的接收其中的呢。
喜欢的点,点个赞呗!