Vuex 的个人回顾

319 阅读6分钟

vuex 个人回顾

该文档为个人回顾,请以官方文档为主

什么是 vuex

  1. vuex 可以理解为整个应用的全局数据(状态)管理中心,实现组件之间数据共享。
  2. vuex 中的数据是响应式的,当数据发生变化时,引用其数据的组件也会得到响应。(就跟组件在使用自己的 data 数据一样)。
  3. vuex 中的 State 状态只能通过 mutation 来修改。

没有 vuex 之前

  1. 在没有使用 Vuex 之前,在多层嵌套组件中传递数据是一件非常痛苦的事情,要一层一层的传递下来,触发事件还得一层层的传递回去才能修改一个数据,这样使代码变得难以维护。
  2. 如果兄弟组件之间共享一个状态更是得将状态提取到父组件中去,非常麻烦。

使用 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')
    1. 创建基于某个命名空间辅助函数.
    2. 返回一个包含绑定在命名空间上的辅助函数的对象。
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 接受三个参数

  1. function(state, getter) 定义一个用于监听 state 或 getter 的函数,它的返回值就是它的监听对象
  2. newVal, oldVal => console.log(newVal, oldVal ) 返回被监听的值被修改后的值,第一个参数为新值,第二个参数为修改前的值
  3. options 可选、表示 Vue 的 vm.$watch 方法的参数(暂时还没试过,先用官方的话表达一下把)

subscribe 监听 mutation 被提交后出发

在每次提交 mutation 并在 mutation 执行之后触发(每次调用都会触发)

语法

该方法接收一个回调函数,回调函数有两个参数分别为

  1. 被调用的 mutation 的 type 和 payload
  2. 经过 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

  1. action 被触发的 action
  2. 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 被创建出来之后,我们还可以动态的创建一个新的模块出来

  1. store.registerModule 注册模块
  2. store.unregisterModule 卸载模块,该方法不能卸载一开始就声明的模块(创建 store 时就声明的模块)
  3. 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)