在项目中扒一些vuex的蛛丝马迹

301 阅读9分钟

如果你想看官方文档我不拦着你

vuex官方文档

我下面很多的概念我也是照搬文档,建议你先看一遍文档再看下面写的

一、什么是vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

先来解释一下什么是状态管理模式?

为了更好地了解状态管理模型,有必要先来了解一下MVVM架构

MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,在这一层可以初始化数据,定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来;ViewModel 是一个同步View 和 Model的对象。

在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间 的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。

ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动

的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

那么vue.js和MVVM架构的关系是什么?

vue.js是MVVM架构的最佳实践,基于这种架构,vue也通过双向数据绑定把 View 层和 Model 层连接了起来,vue通过数据驱动视图的改变,数据变化了,视图也就跟随变化了。

因此在单页面应用中,vue的状态管理模式包含以下几个部分:

  • state,驱动应用的数据源;

  • view,以声明方式将 state 映射到视图;

  • actions,响应在 view 上的用户输入导致的状态变化。

可以看到三者的关系是相互作用的,但是从数据流的方向来看,是单向的。

到了这里,可以思考一个问题了,在多个组件共享一个状态的时候,这个闭环的数据流就很容易被破坏:

  • 多个视图依赖于同一状态。

  • 来自不同视图的行为需要变更同一状态。

等等,你们该不会不知道“状态”是什么吧?

对于问题一

在实际的应用中,一般是通过传参的方式实现不同组件维持对于某个状态的关联性,比如B、C组件同时依赖A组件的某个状态,那A组件的状态改变,需要通过传值参的方式告诉B、C组件

但是,传参的方法对于多层嵌套的组件将会非常繁琐,即便vue为我们提供多种传参的方式

对于问题二

我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝

以上的这些模式非常脆弱,通常会导致无法维护的代码。

基于这样的问题,vuex把组件的共享状态抽取出来,以一个全局单例模式管理,这些共享状态构成一个类似第三方的仓库,各个组件与他建立联系。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,并以相应的规则保证状态以一种可预测的方式发生变化,从而代码将会变得更结构化且易维护。

这就是vuex诞生的使命。

我怎么感觉我写的比官方文档还好?

注意上面提到的“并以相应的规则保证状态以一种可预测的方式发生变化”,也就是说“可预测”是vuex追求的目标,下面就开看看在一个项目中是怎么构建vuex的,还有vuex是怎么实现可预测的

二、项目中怎么构建vuex

前提是已经利用脚手架工具构建好了一个项目的基本目录结构

接下来

1、安装vuex

npm install vuex --save

2、新建store目录

src目录下新建一个store目录,store目录新建以下文件

注意:

在引入文件的时候要注意根据原文件的写法是全部引入还是按需导入

以下代码只做格式参考

  • index.js

    import Vue from 'vue';
    import Vuex  from 'vuex';
    import * as actions from './actions'; // 按需
    import * as getters from './getters'; // 按需
    import mutations from './mutations'; // 全部引入
    Vue.use(Vuex);
    const state = {
    	// 需要维护的状态写在这里
        count: 1,
        isLogin: false
    }
    export default new Vuex.Store({
      state,
      actions,
      getters,
      mutations
    });
    
  • getters.js

    // 获取登录状态
    // 单独暴露
    export const getLoginState = state => {
      return state.isLogin;
    };
    
  • mutations.js

    // import * as types from './mutation-types'; // 常量方式
    // 整体暴露
    export default {
      // 常量方式
      // [types.INCREMENT_COUNT_STATE] (state, value) {
      //   state.count += value;
      // },
      // [types.SET_LOGIN_STATE] (state, value) {
      //   state.isLogin = value;
      // }
      increment (state, value) { // 计数
        state.count += value;
      },
      setLogin (state, value) { // 设置登录状态
        state.isLogin = value;
      }
     };​
    
  • actions.js

    // 计数
    export const increment = ({commit}) => {
      commit ("increment",100);
    }
    // 设置登录状态
    export const setLogin =({commit},value) => {
      commit ("setLogin",value);
    }
    

3、引入store

在应用入口文件main.js中引入store

import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

以上就是vuex在一个项目中的样子,好了,此文结束!

下面开始抄文档...

三、state

1、定义状态

state属性保存着我们的状态,比如上面index.js文件中,我们定义了一个状态count

const state = {    // 需要维护的状态写在这里    count: 1,    islogin: false}

这样我们就有一个集中管理的状态count,那其他组件如何取到这个count呢?

2、获取状态

  • 计算属性

    由于 vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性 中返回某个状态

    computed: {  count () {    return this.$store.state.count   }}
    
  • mapState 辅助函数

    当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性

    首先,在组件中引入 mapState

    import { mapState } from 'vuex'
    
    data() {
      return {
      	localCount: 100
      }
    },
    computed: mapState({
      // 箭头函数可使代码更简练
      count: state => state.count,
      
      // 传字符串参数 'isLogin' 等同于 `state => state.isLogin`
      isLogin: "isLogin"
      
      // 为了能够使用 `this` 获取局部状态,必须使用常规函数
      countPlusLocalState (state) {
      	return state.count + this.localCount
      }
    });
    

    当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组

    computed: mapState([
      // 映射 this.count 为 store.state.count
      // 映射 this.isLogin 为 store.state.isLogin
      'count',
      'isLogin'
    ])
    

    不知道大家有没有发现,使用了mapState 函数返回的是一个对象,computed的结构变化了,在实际的组件中,computed不仅仅是用来获取vuex的状态,还有其他的数据,我们如何将它与局部计算属性混合使用呢?

    借助对象展开运算符

    computed: {
      localComputed () { /* ... */ },
      // 使用对象展开运算符将此对象混入到外部对象中
      ...mapState({
        // ...
      })
    }
    

四、getter

1、定义状态

假设已有状态

const state = {
  // 需要维护的状态写在这里
  todos: [
    { id: 1, text: '...', done: true },
    { id: 2, text: '...', done: false }
  ]
}

现在要做一件事情就是,对列表todos进行过滤并计数:

computed: {
	doneTodosCount () {
		return this.$store.state.todos.filter(todo => todo.done).length
	}
}

如果有多个组件需要用到此属性,我们要么复制这个函数,在多个组件中使用,这会导致代码的冗余,并不优雅

因此,vuex允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

在项目中,关于getter的代码写在getters.js文件中

  export const doneTodos = state => {
   return state.todos.filter(todo => todo.done).length
 }

2、获取状态

  • 直接在插值表达式中使用属性,如{{ doneTodos }}

  • 通过属性访问

    Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

    {{this.$store.getters.doneTodos}}

  • 通过方法来访问

    让getter返回一个函数,来实现给getter传参,在你对数组进行查询时十分有用,

    // 在getters.js文件中
    export const getTodoById = (state) => (id) => {
    	return state.todos.find(todo => todo.id === id)
    }
    
    // 模板中
    this.$store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
    

    注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果

  • mapGetters 辅助函数

    mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

    import { mapGetters } from 'vuex'
    
    export default {
      // ...
      computed: {
      // 使用对象展开运算符将 getter 混入 computed 对象中
        ...mapGetters([
          'doneTodosCount',
          'anotherGetter',
          // ...
        ])
      }
    }
    

    如果你想将一个 getter 属性另取一个名字,使用对象形式:

    ...mapGetters({
      //`this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
      doneCount: 'doneTodosCount'
    })
    

五、mutation

如果你想要修改store中状态,唯一的方法就是提交mutation,vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

1、定义mutation

假设我要该变状态count的值,每次提交,完成计数;

假设我要改变登录的状态,每次提交,改变登录状态为当前状态;

在mutations.js文件中

// 单独暴露
export const increment = (state, value) => state.count += value;
export const setLogin = (state, value) => state.isLogin = value;

// 整体暴露
export default {
  increment (state, value) { // 计数
    state.count += value;
  },
  setLogin (state, value) { // 设置登录状态
    state.isLogin = value;
  }
 };

注意:

使用常量替代 mutation 事件类型,把这些常量放在单独的文件mutation-types.js中可以让开发者对整个 app 包含的 mutation 一目了然,因此在实际的开发中,建议是采用这种模式

mutation-types.js文件

export const INCREMENT_COUNT_STATE = 'INCREMENT_COUNT_STATE';
export const SET_LOGIN_STATE = 'SET_LOGIN_STATE';

然后在mutations.js文件中

import * as types from './mutation-types';
export default {
  [types.INCREMENT_COUNT_STATE] (state, value) {
    state.count += value;
  },
  [types.SET_LOGIN_STATE] (state, value) {
    state.isLogin = value;
  }
};

2、在组件中提交

  • 常规方式

    调用的时候不可以直接调用increment和setLogin方法,而是当你触发一个类型为increment或者setLogin的mutation时,调用对应的函数,这里需要一个唤醒的过程

    因此需要调用 store.commit 方法,传入相应的type:

    this.$store.commit('increment', 100);
    this.$store.commit('setLogin', true);
    
  • 常量方式

    当然如果是使用常量替代 mutation 事件类型的方式,调用过程是这样的:

    首先:

    在你要使用的组件中引入mutation-types.js,如

    import * as types from '../store/mutation-types'
    

    第二步:

    this.$store.commit(types.INCREMENT_COUNT_STATE, 100);
    this.$store.commit(types.SET_LOGIN_STATE, true);
    
  • mapMutations 辅助函数

    mapMutations 辅助函数将组件中的 methods 映射为 this.$store.commit 调用

    首先,引入mapMutations

    import { mapMutations } from 'vuex'
    

    第二步:

    export default {
        //....
      created() {
        this.increment(100);
        this.setLogin(true);
      },
      methods: {
        ...mapMutations([
          'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
    
          // `mapMutations` 也支持载荷:
          'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
        ]),
        ...mapMutations({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
        })
      }
    }
    

3、提交载荷

  • 直接参数作为载荷

    你可以向 store.commit 传入额外的参数,即 mutation 的 载荷

    // 定义
    export const increment = (state, n) => state.count += n;
    
    // 提交
    this.$store.commit('increment', 100);
    
    // 读取
    {{ this.$store.state.count }} // 101
    
  • 对象作为载荷

    在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

    // 定义
    export const increment = (state, payload) => state.count += payload.amount;
    
    // 提交
    this.$store.commit('increment', {
      amount: 100
    });
    
    // 读取
    {{ this.$store.state.count }} // 101
    

    注意:

    提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

    // 提交
    this.$store.commit({
      type: 'increment',
      amount: 10
    });
    

3、关于响应式

既然 vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  1. 最好提前在你的 store 中初始化好所有所需属性。

  2. 当需要在对象上添加新属性时,你应该

  • 使用 Vue.set(obj, 'newProp', 123), 或者

  • 以新对象替换老对象。例如,利用[对象展开运算符我们可以这样写:

    state.obj = { ...state.obj, newProp: 123 }
    

4、mutation必须是同步函数

假设mutation中的某一类型的回调函数是异步函数,如下,会产生什么样的影响?

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

我们来回顾一下vuex出现是为了解决什么问题:

一是:采用集中式存储管理应用的所有组件的状态

二是:并以相应的规则保证状态以一种可预测的方式发生变化

vuex在管理组件的状态过程中,要以相应的规则保证状态以一种可预测的方式方式变化。为了实现可预测,那么保证的规则之一就是:

mutation必须时同步函数

你可以想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的

六、action

首先,明确的是:

  • Action提交的是mutation,而不是直接变更状态

  • Action可以包含任意的异步操作

1、定义action

// 计数
export const increment = (context) => {
  context.commit ("increment",100);
}
// 设置登录状态
export const setLogin =(context,value) => {
  context.commit ("setLogin",value);
}

action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters

当我们需要调用 commit 很多次的时候,可以使用解构的写法

// 计数
export const increment = ({commit}) => {
  commit ("increment",100);
}
// 设置登录状态
export const setLogin =({commit},value) => {
  commit ("setLogin",value);
}

2、分发action

  • 常规方式

    this.$store.dispatch("increment");
    this.$store.dispatch("setLogin",true);
    
  • mapActions 辅助函数

    将组件的 methods 映射为 store.dispatch 调用

    首先,引入mapActions

    import { mapActions } from 'vuex'
    

    第二步

    export default {
      created () {
      	this.increment()
      },
      methods: {
        ...mapActions([      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`      // `mapActions` 也支持载荷:      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`    ]),
        ...mapActions({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        })
      }
    }
    

3、组合action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

因此,你可以

store.dispatch('actionA').then(() => {
  // ...
})

在另外一个 action 中也可以:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

最后,如果我们利用 async / await (opens new window),我们可以如下组合 action:

// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。