Vuex 的个人回顾

·  阅读 254

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)
复制代码
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改