27 Vuex相关
vuex是什么?怎么使用?哪种功能场景使用它?
Vuex是一个专为Vue.js应用程序开发的状态管理模式。vuex就是一个仓库,仓库里放了很多对象。其中state就是数据源存放地,对应于一般 vue 对象里面的data里面存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这相数据的组件也会发生更新它通过mapState把全局的state和getters映射到当前组件的computed计算属性
vuex一般用于中大型web单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用vuex的必要性不是很大,因为完全可以用组件prop属性或者事件来完成父子组件之间的通信,vuex更多地用于解决跨组件通信以及作为数据中心集中式存储数据。- 使用
Vuex解决非父子组件之间通信问题vuex是通过将state作为数据中心、各个组件共享state实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于State中能有效解决多层级组件嵌套的跨组件通信问题
vuex的State在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在State中,并在Action中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在State中呢? 目前主要有两种数据会使用vuex进行管理:
- 组件之间全局共享的数据
- 通过后端异步请求的数据

包括以下几个模块
state:Vuex使用单一状态树,即每个应用将仅仅包含一个store实例。里面存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这相数据的组件也会发生更新。它通过mapState把全局的state和getters映射到当前组件的computed计算属性mutations:更改Vuex的store中的状态的唯一方法是提交mutationgetters:getter可以对state进行计算操作,它就是store的计算属性虽然在组件内也可以做计算属性,但是getters可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用gettersaction:action类似于muation, 不同在于:action提交的是mutation,而不是直接变更状态action可以包含任意异步操作modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex的store对象分割成模块(modules)

modules:项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理

回答范例
思路
- 给定义
- 必要性阐述
- 何时使用
- 拓展:一些个人思考、实践经验等
回答范例
Vuex是一个专为Vue.js应用开发的状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。- 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 - > 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是
vuex存在的必要性,它和react生态中的redux之类是一个概念 Vuex解决状态管理的同时引入了不少概念:例如state、mutation、action等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用Vuex反而是繁琐冗余的,一个简单的store模式就足够了。但是,如果要构建一个中大型单页应用,Vuex基本是标配。- 我在使用
vuex过程中感受到一些等
可能的追问
vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
- 刷新浏览器,
vuex中的state会重新变为初始状态。解决方案-插件vuex-persistedstate
action和mutation的区别是什么?为什么要区分它们?
action中处理异步,mutation不可以mutation做原子操作action可以整合多个mutation的集合mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测)$watch严格模式下会报错action异步操作,可以获取数据后调佣mutation提交最终数据
- 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发
Action,Action再触发Mutation`。 - 基于流程顺序,二者扮演不同的角色:
Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求 - 角色不同,二者有不同的限制:
Mutation:必须同步执行。Action:可以异步,但不能直接操作State
Vuex中actions和mutations有什么区别
题目分析
mutations和actions是vuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。- 我们只需记住修改状态只能是
mutations,actions只能通过提交mutation修改状态即可
回答范例
- 更改
Vuex的store中的状态的唯一方法是提交mutation,mutation非常类似于事件:每个mutation都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action类似于mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态 - 开发时,包含异步操作或者复杂业务组合时使用
action;需要直接修改状态则提交mutation。但由于dispatch和commit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。调用dispatch和commit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果 - 实现上
commit(type)方法相当于调用options.mutations[type](state);dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了
实现
我们可以像下面这样简单实现commit和dispatch,从而辨别两者不同
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
// 传入上下文和参数1都是state对象
this.options.mutations[type].call(this.state, this.state, payload)
}
dispatch(type, payload) {
// 传入上下文和参数1都是store本身
this.options.actions[type].call(this, this, payload)
}
}
怎么监听vuex数据的变化
分析
vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。- 既然状态都是响应式的,那自然可以
watch,另外vuex也提供了订阅的API:store.subscribe()
回答范例
- 我知道几种方法:
- 可以通过
watch选项或者watch方法监听状态 - 可以使用
vuex提供的API:store.subscribe()
watch选项方式,可以以字符串形式监听$store.state.xx;subscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中
实践
watch方式
const app = createApp({
watch: {
'$store.state.counter'() {
console.log('counter change!');
}
}
})
subscribe方式:
store.subscribe((mutation, state) => {
if (mutation.type === 'add') {
console.log('counter change in subscribe()!');
}
})
Vuex 页面刷新数据丢失怎么解决
体验
可以从localStorage中获取作为状态初始值:
const store = createStore({
state () {
return {
count: localStorage.getItem('count')
}
}
})
业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅
store.commit('increment')
localStorage.setItem('count', store.state.count)
回答范例
vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来localStorage就很合适,提交mutation的时候同时存入localStorage,store中把值取出作为state的初始值即可。- 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用
vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。 - 类似的插件有
vuex-persist、vuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化
原理
可以看一下[vuex-persist (opens new window)](https://github.com/championswimmer/vuex- persist/blob/master/src/index.ts#L277)内部确实是利用subscribe实现的
Vuex 为什么要分模块并且加命名空间
- 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,
store对象就有可能变得相当臃肿。为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块 - 命名空间 :默认情况下,模块内部的
action、mutation和getter是注册在全局命名空间的——这样使得多个模块能够对同一mutation或action作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true的方式使其成为带命名空间的模块。当模块被注册后,它的所有getter、action及mutation都会自动根据模块注册的路径调整命名
你有使用过vuex的module吗?
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
- 用过
module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护 - 可以按之前规则单独编写子模块代码,然后在主文件中通过
modules选项组织起来:reateStore({modules:{...}}) - 不过使用时要注意访问子模块状态时需要加上注册时模块名:
store.state.a.xxx,但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace选项,此时再访问它们就要加上命名空间前缀。 - 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。
pinia显然在这方面有了很大改进,是时候切换过去了
你觉得vuex有什么缺点
分析
相较于redux,vuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
回答范例
vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档- 比如:访问
state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,getters,mutations,actions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。 - 之前
Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合
原理
下面我们来看看vuex中store.state.x.y这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态
parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {
// 获取父模块state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 把子模块state设置到父模块上
parentState[moduleName] = module.state
})
}
用过pinia吗?有什么优点?
1. pinia是什么?
在
Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:Pinia是Vue的存储库,它允许您跨组件/页面共享状态。 * 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3的Vuex
2. 为什么要使用pinia?
Vue2和Vue3都支持,这让我们同时使用Vue2和Vue3的小伙伴都能很快上手。pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。pinia中action支持同步和异步,Vuex不支持- 良好的
Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了 - 无需再创建各个模块嵌套了,
Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。 - 体积非常小,只有
1KB左右。 pinia支持插件来扩展自身功能。- 支持服务端渲染
3. pinna使用
- 准备工作
我们这里搭建一个最新的Vue3 + TS + Vite项目
npm create vite@latest my-vite-app --template vue-ts
pinia基础使用
yarn add pinia
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
2.1 创建store
//sbinsrc/store/user.ts
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})
创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:
name:一个字符串,必传项,该store的唯一id。options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。
我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的
2.2 使用store
<!-- src/App.vue -->
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>
2.3 添加state
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
});
2.4 读取state数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>
上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
2.5 修改state数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
<button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
store.name = "张三";
console.log(store);
};
</script>
2.6 重置state
- 有时候我们修改了
state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。 - 此时,我们直接调用
store的$reset()方法即可,继续使用我们的例子,添加一个重置按钮
<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};
当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新
2.7 批量更改state数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store的$patch方法,修改app.vue代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
- 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们
state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。 - 为了解决该问题,
pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
2.8 直接替换整个state
pinia提供了方法让我们直接替换整个state对象,使用store的$state方法
store.$state = { counter: 666, name: '张三' }
上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少
getters属性
getters是defineStore参数配置项里面的另一个属性- 可以把
getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样
3.1 添加getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 10,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
},
})
上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template>
<p>新年龄:{{ store.getAddAge }}</p>
<button @click="patchStore">批量修改数据</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
</script>
上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter中调用其它getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
3.3 getter传参
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
<p>新年龄:{{ store.getAddAge(1100) }}</p>
actions属性
- 前面我们提到的
state和getters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。 - 那么,如果我们有业务代码的话,最好就是卸载
actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。 actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
4.1 添加actions
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
saveName(name: string) {
this.name = name;
},
},
});
4.2 使用actions
使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法
const saveName = () => {
store.saveName("poetries");
};
总结
pinia的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌
pinia无非就是以下3个大点:
stategettersactions
