vuex 个人回顾
该文档为个人回顾,请以官方文档为主
什么是 vuex
- vuex 可以理解为整个应用的全局数据(状态)管理中心,实现组件之间数据共享。
- vuex 中的数据是响应式的,当数据发生变化时,引用其数据的组件也会得到响应。(就跟组件在使用自己的 data 数据一样)。
- vuex 中的 State 状态只能通过 mutation 来修改。
没有 vuex 之前
- 在没有使用 Vuex 之前,在多层嵌套组件中传递数据是一件非常痛苦的事情,要一层一层的传递下来,触发事件还得一层层的传递回去才能修改一个数据,这样使代码变得难以维护。
- 如果兄弟组件之间共享一个状态更是得将状态提取到父组件中去,非常麻烦。
使用 vuex 之后
不管处于组件树的哪一位置,都能直接获取存放在 vuex 中的数据或触发某些行为,不需要再通过父组件去层层传递了。
只需维护一份代码,不需因为业务需求改变后导致所有组件都需修改一遍
安装
// script 标签引入
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
// npm 引入
npm install vuex --save
// yarn 引入
yarn add vuex
Promise 支持
vuex 依赖 Promise。有些浏览器并没有实现 Promise,可以引入一个 polyfill 的库,如 es6-promise。
// script 标签,在使用 vuex 之前引入
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
// 安装
npm install es6-promise --save # npm
yarn add es6-promise # Yarn
// 在 main.js 中的 vuex 之前引入
import 'es6-promise/auto'
核心
vuex 的核心就是 store(仓库)
全局使用
在 main.js 入口文件中引入 store 仓库,并将 store 注入到 Vue 中。
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
new Vue({
store,
}).$mount('#app')
// 在组件中使用
this.$store.state['属性名']
vuex(Store)的基本结构
// 1、引入
import Vue from 'vue';
import Vuex from 'vuex';
// 2、使用
Vue,use(Vuex);
// 3、创建
const store = new Vuex.store({
// 数据存放中心
state: {},
// 用来获取数据的方法,可以对数据进行处理后返回
getters: {},
// 修改 state 的唯一方法,在其中不能使用异步,只能是同步操作
mutations:{},
// 可以在这里处理(异步)数据后提交给 mutations,让 mutations 去修改 state
actions: {}
})
// 暴露出去
export default store
State
Vuex 中的 State 与 Vue 中的 data 基本一致,都是响应式的。
语法
export default new Vuex.Store({
state: {
count: 1
}
// 模块中的写法(后面的 modules 内容,没学到可以跳过)
// 其实还是和组件一样,返回一个对象来防止引用同一份数据造成冲突
state: () => ({
count: 1
})
})
// 在组件中使用
computed: {
count(){
return this.$store.state.count
}
}
// 如果没有在全局中注入 store,就手动引入 store 后使用,效果一样
import store from 'vuex';
computed: {
count(){
return store.state.count
}
}
mapState 辅助函数
自动帮我们生成计算属性。 有两种写法:
- 数组写法: 如果 State 的名字和计算属性的名字一样,且无需其他操作,那么直接使用数组写法就可以
- 对象写法: 想给 State 的属性换个名或对属性进行其他操作,就可以使用对象写法
import { mapState } from 'vuex';
export default {
data(){
return {
number: 1
}
},
computed: {
// 数组写法
...mapState([
'count'
]),
// 对象写法
...mapState({
// 给 count 换个名称
newFields: 'count',
// 返回 count 与 number 相加后的值
increment: state => {
return this.number + state.count
}
})
}
}
Getters
getters 与 Vue 中的计算属性 差不多,它的返回值会根据它的依赖缓存起来,依赖被修改时它会被重新计算。
语法
// store.js
const store = new Vuex.Store({
state: {
todos: [...],
}
getters: {
// state 为 store 中的 State
todoLength: (state) => {
// 获取 todos 数组的长度
return state.todos.length
},
// getters 为 store 中的所有 getters
getTodos: (state, getters) => {
return {
data: state.todos,
// 获取 getters 中 todoLength 的结果,相当于调用另外一个计算属性的结果
count: getters.todoLength,
}
}
}
})
作用
从 State 中派生出一些新的状态(数据)
以上面语法的例子为例,我们已知有 todos 这个数组,当我们有很多组件都需要获取到它的长度时:
- 不使用 getter 的情况下,我们每个组件都要写计算属性来获取它,如果需求更改那么所有组件都需要更改,维护性极差。
- 使用 getter 后,我们只需要在 store 中维护一份 getter 就可以满足所有组件的需求
通过传参的方式筛选数据
getter 也可以返回一个函数,用户可以通过对该函数传入参数来获取相应的数据。
getter 在通过方法访问时,不会缓存结果。
// store.js
getters: {
// getter 返回一个函数,用户可以通过对该函数传参筛选出对应数据
getFinishTodos: state => done => {
return state.todos.find(item => item.done === done)
}
}
mapGetters 辅助函数
将 store 中的 getter 映射到组件的计算属性中
// 引入 mapGetters
import { mapGetters } from 'vuex';
export default {
computed: {
// 写法一 数组写法 如果计算属性的名字与 getter 的名字一样,可以使用数组写法
...mapGetters(['getFinishTodos']),
// 写法二 对象写法 如果你想给 getter 换一个名字,可以使用对象写法
...mapGetters({
length: 'todoLength',
})
}
}
Mutation
mutation 是更改 store 中 State 数据的唯一方法
语法
mutation 的使用方法为使用commit提交。
this.$store.commit('mutationName', '用户传参')
// 声明 matation 语法
mutations: {
// state 为 store 中的 State
// payload 为用户传参(非必填)
increment(state, payload){
state.count += payload.number
}
}
// 调用语法
// type 为 mutationName
// payload 为用户传参
this.$store.commit(type, payload)
// 或
this.$store.commit({
type: 'mutationName',
payload: { number: 1 }
})
注意
- mutation 必须是同步的
- 不能直接调用 mutation,比如错误写法:
this.$store.mutations.increment - 必须使用 commit 提交 mutation,比如:this.$store.commit('mutationName', '用户传参')
mapMutations 辅助函数
使用 mapMutations 函数可以帮我们将 mutation 映射到组件的 methods 中。
// 在组件中引入 mapMutations
import { mapMutations } from 'vuex';
export default {
methods: {
// 如果方法名与 mutation 的名称一样,可以直接用数据把 mutation 映射过来
...mapMutations([
// 相当于将 `this.increment()` 映射为 `this.$store.commit('increment')`; (官方原话)
// 如果存在传参也可以这么写,在调用的时候一起传过去就可以,例如:
// this.increment(number) 等于 this.$store.commit('increment', number)
'increment',
]),
// 对象写法
...mapMutations({
// 如果名称我们想定义的名称与 mutations 的名称不一致,我们可以重新赋予一个名称
'add': 'increment', // 这样我们在调用的时候就可以使用 this.add(),来提交一个 mutation 了。
})
}
}
Action
Action 用于提交 mutation 去修改 State,Action 本身不直接修改 State。
流程:组件 -> Action -> Mutation -> State
Action 与 Mutation 的区别
- Action 提交的是 mutation,不直接修改 state
- Action 可以包涵异步,Mutation 只允许同步
语法
Action 函数接收一个 context 对象,与一个 payload(用户传参),context 对象拥有与 store 一致的属性和方法,但不是 store 本身。
触发 Action 的方法:this.$store.dispatch 是触发 Action 的唯一途径。
// store.js
actions: {
actionName (context, payload){
let {commit, state, dispatch, getters } = context;
},
}
// 组件中触发 Action 的方法
// dispatch 是是触发 Action 的唯一途径
this.$store.dispatch('actionName', payload); // payload 为自定义参数
// or 下面是对象写法,上面写法同等于下面写法
this.$store.dispatch({ type: 'increment', number: 1 })
一个简单的 Demo
组件触发 Action,Action 提交 Mutation,Mutation 修改 State。
// store.js
const store = new Vuex.store({
state: {
count: 0
},
mutations: {
increment: state => {
state.count ++
}
},
actions: {
increment: (context) {
context.commit('increment'); // 通过提交 mutation 来使 state 中的 count + 1
}
}
})
// 组件
this.$store.dispatch('increment')
dispatch 方法会返回一个 Promise
dispatch 方法默认返回一个 Promise 对象,也就是说我们可以主动的在 Action 中返回一个 Promise 给 dispatch,dispatch 就会继续把该 Promise 返回给调用者。调用者就可以通过该返回来监听 Action 的执行情况。
// store.js
actions: {
increment(context){
return new Promise((resolve, reject) => {
setTimeOut(() => {
context.commit('increment');
resolve(true)
}, 1000)
})
}
}
// 组件
this.$store.dispatch('increment').then(res => console.log(res)); // true
mapActions 辅助函数
mapActions 可以将 store.js 中的 action 映射到组件的 methods 中
import { mapActions } from 'vuex';
export default {
methods: {
// 数组写法
...mapActions([
'increment'
]),
// 对象写法
...mapActions({
addCount: 'increment'
})
}
}
Module
vuex 提供模块分割,允许我们将 store 分割为多个模块,每个模块拥有自己的 State、Actions、Mutations、Getters、嵌套。避免所有 Store 过于臃肿。
通过 Demo 理解
创建两个模块:moduleA 、 moduleB。
注意模块中 State 的写法。
// moduleA
export default {
state: () => ({
count: 1
}),
mutations: {
increment(state){
state.count ++
}
},
actions: {
increment({commit}){
setTimeout(() => commit('increment'), 1000)
}
}
}
// moduleB
export default {
state: () => ({
todos: [{
id: 1,
name: '待办事项1',
done: true
},{
id: 2,
name: '待办事项2',
done: true
}]
}),
getters: {
todoLength(state){
return state.todos.length
}
}
}
模块准备好后将其引入 Store 中
// store.js
import moduleA from './moduleA';
import moduleB from './moduleB';
export default new Vuex.Store({
modules: {
moduleA,
moduleB
}
})
现在模块已经被引入 Store 中。那么在组件这么使用?(非命名模块下)
- 除了 State 外,Getter、Mutation、Action 的使用方法与不分模块前一致。
- State 的使用需要带上模块名称。
// 组件
export default{
computed: {
// 获取模块 moduleA state 中的 count 数据
count(){
return this.$store.state.moduleA.count
},
// 获取模块 moduleB getters 中的 todoLength 数据
todoLength(){
return this.$store.getters.todoLength
}
},
methods: {
// 提交 moduleA 的 mutation,不使用局部命名空间的情况下与不分割模块用法一致
increment(){
this.$store.commit('increment')
},
// 提交 moduleA 的 actions,不使用局部命名空间的情况下与不分割模块用法一致
asyncIncrement(){
this.$store.dispatch('increment')
}
}
}
当不同模块出现重复名称的 mutation 或 action 那么他们是共存的,当 commit/dispatch 时,两个都会被触发
命名空间
在没有使用命名空间之前,所有的 Actions、Mutations、Getters 都是注册在全局命名空间之下的。如果出现同名的 Action 或 Mutation。那么他们是共存的,当有组件调用时他们会同时被触发。这样可能会造成一些不可预估的 BUG。
使用命名空间就相当于将他们独立隔离起来,需要通过对应的路径来访问。
语法
为模块加上 namespaced: true 属性即可。
// moduleA
export default {
namespaced: true,
state: ...,
getters: ...
}
访问方法
加上模块名称访问
// 在组件中提交 moduleA 的 mutations 的 increment
this.$store.commit('moduleA/increment')
// 获取 moduleB 的 getters 的 todoLength
this.$store.getters['moduleB/todoLength']
// 触发 moduleA 的 actions 的 increment
this.$store.dispatch('moduleA/increment')
在模块内提交或触发全局模块或其他模块的 action、mutation
为 dispatch\commit 添加第三个参数:{ root: true}
// 触发全局 action
dispatch('increment', null, { root: true })
// 提交全局 mutaiotn
commit('increment', null, { root: true })
在带命名空间的模块中注册全局 action
在带命名空间的模块中注册 action 都是属于模块内部(局部)的。
下面是在模块中注册全局 action 的方法。
export default {
// ...
actions: {
// 将 action 函数修改为以下对象
// theAction 依旧是 action 的函数名
// 对象中 root: true 表示将其注册在全局(根)下面
// handler 为 action 的处理函数,handler 函数中有两个参数 namespacedContext, payload
// payload 载荷
theAction: {
root: true,
handler (namespacedContext, payload) {
let {commit, dispatch, getters, rootGetters, rootState, state} = namespacedContext;
}
}
}
}
在组件中使用带命名空间的 mapState、mapMutations、mapGetters、mapActions
最普通的用法,每一个都加上路径。
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
component: {
...mapState({
count: state => state.moduleA.count
}),
...mapGetters({
todoLength: 'moduleB/todoLength'
}),
...mapMutation({
increment: 'moduleA/increment'
}),
...mapActions({
increment: 'moduleA/increment'
})
}
提取模块路径
computed: {
...mapState('moduleA', {
count: state => state.count
}),
...mapGetters('moduleB', {
todoLength: 'todoLength'
}),
...mapMutation('moduleA', [
'increment'
])
...mapActions('moduleA', [
'increment'
])
}
使用 createNamespcacedHelpers 函数
- createNamespacedHelpers('modeleName')
- 创建基于某个命名空间辅助函数.
- 返回一个包含绑定在命名空间上的辅助函数的对象。
const { mapState: TodoMapState, mapActions } = createNamespacedhelpers('moduleA');
export default{
computed:{
...TodoMapState([
'count'
])
}
}
Vuex 中的插件
vuex插件是一个函数,接收 store 作为唯一参数。它将挂载在 Store 中。
语法
// 创建一个插件
const thePlugin = store => {
// 通过 store 去做一些操作
// 比如去监听 mutation 的提交
store.subscribe((mutation, state) => {})
}
const store = new Vuex.Store({
// 挂载插件
plugins: [thePlugin]
})
Vuex 表单处理
如果表单的数据是存放在 Store 中的,那么就不要使用 v-model,因为 v-model 会直接修改 State,在严格模式中会抛出异常。
解决方案:
- 使用 :value + @event(事件) + mutation 之类的事件去修改。
- 使用计算属性 set、get 写法。
computed:{
username: {
get(){
return this.$store.state.formData.username
},
set(value){
this.$store.commit('inputUsername', value)
}
}
}
其他 API
watch
监听 Store 中 State 的改变
const unWatchFn = store.watch(
(state, getters) => {},
(newVal, oldVal) => {},
options?:Object
): Function
// 取消监听
unWactchFn()
该 API 接受三个参数
- function(state, getter) 定义一个用于监听 state 或 getter 的函数,它的返回值就是它的监听对象
- newVal, oldVal => console.log(newVal, oldVal ) 返回被监听的值被修改后的值,第一个参数为新值,第二个参数为修改前的值
- options 可选、表示 Vue 的 vm.$watch 方法的参数(暂时还没试过,先用官方的话表达一下把)
subscribe 监听 mutation 被提交后出发
在每次提交 mutation 并在 mutation 执行之后触发(每次调用都会触发)
语法
该方法接收一个回调函数,回调函数有两个参数分别为
- 被调用的 mutation 的 type 和 payload
- 经过 mutation 处理之后的 state
const unSubscribe = store.subscribe((mutation, state) => {
let { type, payload } = mutation;
})
// 卸载监听
unSubscribe()
该监听函数返回一个可卸载监听的回调,如需卸载可以执行该回调。
执行顺序提升
如果程序有多个监听函数,那么函数的执行前后顺序是以声明的位置排列的,越早声明越先被执行。如果想让后面的函数先执行,除了调换声明位置,还可以传入一个参数使其在第一个被执行:{ prepend: true }
// 该监听会在所有监听中最先被执行
store.subscript(handler, { prepend: true });
subscribeAction 监听 action 被触发
每次触发 action 之前或之后(可配置),都会触发该监听函数
subscribeAction 接收一个回调函数,该回调函数有两个参数 action,state
- action 被触发的 action
- state 当前的 State,注意:是没有被 action 调用 mutation 修改前的 State
const unSubscribeAction = store.subscribeAction((action, state) => {
let { type, payload } = action;
console.log(state); // 未被该 action 提交 mutation 修改前的 state
})
// 卸载监听
unSubscribeAction();
该监听函数返回一个可卸载监听的回调,如需卸载可以执行该回调。
执行顺序提升
如果程序有多个监听函数,那么函数的执行前后顺序是以声明的位置排列的,越早声明越先被执行。如果想让后面的函数先执行,除了调换声明位置,还可以传入一个参数使其在第一个被执行:{ prepend: true }
store.subscribeAction(() => ...) // 第一个执行
store.subscribeAction(() => ...) // 第二个执行
store.subscribeAction(() => ...) // 第三个执行
// 按顺序这个应该是第四个执行,但是他传了 prepend: true,那个他将变成第一个执行
store.subscribeAction(() => {}, { prepend: true })
指定监听函数的执行时间
subscribeAction 可以指定处理函数在 action 分发之前还是之后执行。
注意:不管是分发前还是分发后, state 都是未修改前的 state,不会因为是 after 就是处于之后 state
store.subscribeAction({
before: (action, state) => {
console.log('before Action')
},
after: (action, state) => {
console.log('afterAction')
}
})
捕获 action 抛出的错误
在 3.4.0 起,可以指定一个 error 函数,用于捕获 action 抛出的错误
store.subscribeAction({
error: (action, state, error) => {
console.log(`error action ${action.type})
console.error(error)
}
})
动态模块
在 store 被创建出来之后,我们还可以动态的创建一个新的模块出来
- store.registerModule 注册模块
- store.unregisterModule 卸载模块,该方法不能卸载一开始就声明的模块(创建 store 时就声明的模块)
- store.hasModule(moduleName) 检测模块是否已被注册(检测模块是否存在)
import Vuex from 'vuex';
const store = new Vuex.Store({
// ...
})
// 模块是否存在
store.hasModule('moduleName');
// 注册模块
store.registerModule('moduleName', {
// 模块内容
})
// 嵌套模块注册 `moduleName/childModuleName`
store.registerModule(['moduleName', 'childModuleName'], {
// 模块内容
})
// 卸载模块
store.unregisterModule(moduleName)