【Vuex 源码学习】第五篇 - Vuex 中 Mutations 和 Actions 的实现

1,398 阅读4分钟

这是我参与8月更文挑战的第31天,活动详情查看:8月更文挑战


一,前言

上一篇,主要介绍了 Vuex 中 getters 的实现,主要涉及以下几个点:

  • 将选项中的 getters 方法,保存到 store 实例中的 getters 对象中;
  • 借助 Vue 原生 computed,实现 Vuex 中 getters 的数据缓存功能;

本篇,继续介绍 Vuex 中 Mutations 的实现;


二,前文回顾

在前面介绍了 Vuex 的基本使用:

mutation:

  • 同步更新 state 状态;
  • 通过 $store.commit(type, payload)调用 mutations 中对应的方法; action:
  • 异步更新 state 状态;
  • 通过 $store.dispatch(type, payload)调用 actions 中对应的方法;
  • 在 action 方法中,可以进行异步请求操作;
  • 在 action 方法中,可以继续执行 dispatch 方法,调用其他 action 方法;
  • 在 action 方法中,可以多次调用 commit 方法,执行状态更新操作;
// App.vue

<template>
  <div id="app">
    商品数量: {{this.$store.state.num}} 个<br>
    商品单价: 10 元<br>
    订单金额: {{this.$store.getters.getPrice}} 元<br>
    <button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>
    <button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>
  </div>
</template>

在 Store 容器中,mutations 和 actions 相关配置如下:

// src/store/index.js

import Vue from 'vue';
import Vuex from '@/vuex';

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    num: 10
  },
  getters: {
    getPrice(state) {
      return state.num * 10
    }
  },
  // 同步更新状态
  mutations: {
    changeNum(state, payload) {
      state.num += payload;
    }
  },
  // 可执行异步操作,通过 mutations 更新状态
  actions: {
    changeNum({ commit }, payload) {
      setTimeout(() => {
        commit('changeNum', payload)
      }, 1000);
    }
  }
});
export default store;

三,Mutations 的实现

1,将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象

// src/vuex/store.js

export class Store {
  constructor(options) { // options:{state, mutation, actions}
    // 声明 store 实例中的 mutations 对象
    this.mutations = {};
    // 获取 options 选项中的 mutations 对象
    const mutations = options.mutations;

    // 将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象
    Object.keys(mutations).forEach(key => {
      // payload:commit 方法中调用 store 实例中的 mutations 方法时传入
      this.mutations[key] = (payload) => mutations[key](this.state, payload);
    });
  }
}

2,创建并实现 commit 方法

1)用户可以在页面执行调用 store 实例中的 commit 方法:

<button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>

2)也可以在 action 方法中,解构 store 实例中的 commit 方法:

// src/store/index.js

  actions: {
    changeNum({ commit }, payload) {
      setTimeout(() => {
        commit('changeNum', payload)
      }, 1000);
    }
  }

基于以上两种使用场景,创建 commit 方法,为了确保 this 指向 store 实例,commit 方法使用箭头函数:

// src/vuex/store.js

  /**
   * 通过 type 找到 store 实例的 mutations 对象中对应的方法,并执行
   *    用户可能会解构使用{ commit }, 也有可能在页面使用 $store.commit,
   *    所以,在实际执行时,this是不确定的,{ commit } 写法 this 为空,
   *    使用箭头函数:确保 this 指向 store 实例;
   * @param {*} type mutation 方法名
   * @param {*} payload 载荷:值或对象
   */
  commit = (type, payload) => {
    // 执行 mutations 对象中对应的方法,并传入 payload 执行
    this.mutations[type](payload)
  }

当调用 commit 方法时,会根据传入的 type 值(即选项中定义的 mutation 方法名)到 store 实例中的 mutations 对象找到对应的方法(即选项中定义的 mutation 方法),传入 state 和 payload 执行此方法,实现状态的同步更新;

// src/store/index.js

new Vuex.Store({
  ...
  mutations: {
    changeNum(state, payload) {
      console.log(`进入 mutations-changeNum:state = ${JSON.stringify(state)}, payload = ${payload}`)
      state.num += payload;
    }
  }
  ...
});

测试同步更新,查看控制台输出:

image.png

打印 log,执行 mutation 方法;

同时,响应式数据的变化,触发了依赖收集中相关视图的更新渲染;


四,Actions 的实现

对比 dispatch 和 commit 两个方法,可以看出,参数是一致的,实现方法也大致相同:

<button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>
<button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>

1,将 options.actions 中定义的方法,绑定到 store 实例中的 actions 对象

这部分逻辑和 Mutations 是完全一致的

// src/vuex/store.js

export class Store {
  constructor(options) {
    // 声明 store 实例中的 actions 对象
    this.actions = {};
    // 获取 options 选项中的 actions 对象
    const actions = options.actions;
    
    // 将 options.actions 中定义的方法,绑定到 store 实例中的 actions 对象
    Object.keys(actions).forEach(key => {
      // payload:dispatch 方法中调用 store 实例中的 actions 方法时传入
      this.actions[key] = (payload) => actions[key](this, payload);
    });
  }

2,创建并实现 dispatch 方法

1)用户可以在页面执行调用 store 实例中的 dispatch 方法:

<button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>

2)也可以在 action 方法中,解构 store 实例中的 dispatch 方法:

// src/store/index.js

  actions: {
    changeNum({ commit, dispatch }, payload) {
      setTimeout(() => {
        commit('changeNum', payload)
      }, 1000);
    }
  }

基于以上两种使用场景,创建 dispatch 方法,为了确保 this 指向 store 实例,dispatch 方法使用箭头函数:

// src/vuex/store.js

  /**
   * 通过 type 找到 store 实例的 actions 对象中对应的方法,并执行
   *    用户可能会解构使用{ dispatch }, 也有可能在页面使用 $store.dispatch,
   *    所以,在实际执行时,this 是不确定的,{ dispatch } 写法 this 为空,
   *    使用箭头函数:确保 this 指向 store 实例;
   * @param {*} type action 方法名
   * @param {*} payload 载荷:值或对象
   */
  dispatch = (type, payload) => {
    // 执行 actions 对象中对应的方法,并传入 payload 执行
    this.actions[type](payload)
  }

当调用 dispatch 方法时,会根据传入的 type 值(即选项中定义的 action 方法名)到 store 实例中的 actions 对象找到对应的方法(即选项中定义的 action 方法),传入 store实例 和 payload 执行此方法,实现状态的同步更新;

同时,在 action方法中可以再次执行 dispatch 方法执行其他异步操作,也可以多次执行 commit 执行状态的更新操作;

// src/store/index.js

new Vuex.Store({
  ...
  actions: {
    changeNum({ commit }, payload) {
      console.log(`进入 actions-changeNum:commit = ${JSON.stringify(commit)}, payload = ${payload}`)
      setTimeout(() => { // 模拟异步
        commit('changeNum', payload)
      }, 1000);
    }
  }
  ...
});

测试异步更新,查看控制台输出:

image.png

打印 log,先执行 action 方法,再通过 commit 执行 mutation 方法;

同时,响应式数据的变化,触发了依赖收集中相关视图的更新渲染;


五,对比 Mutations 和 Actions

通过以上 Mutations 和 Actions 实现可以看出,不管是同步还是异步更新 State 状态,做种都必须通过 commit 调用 mutation 方法,完成状态的更新操作;

这也使得 Vuex 状态的变更能够被 devtools 所跟踪和记录,实现了状态的统一管理;

vuexLogo.png

再次强调,只能通过 mutation 方法才可以更新 Vuex 中的状态;


六,结尾

本篇,主要介绍了 Vuex 中 Mutations 和 Actions 的实现,主要涉及以下几个点:

  • 将 options 选项中定义的 mutation 方法绑定到 store 实例的 mutations 对象;
  • 创建并实现 commit 方法;
  • 将 options 选项中定义的 action 方法绑定到 store 实例的 actions 对象;
  • 创建并实现 dispatch 方法;

至此,一个简易版的 Vuex 状态管理插件就完成了;

8 月更文的 31 篇文章就算水完了;9 月开始 Vue2.x 源码的第二轮学习-文章优化;Vuex 下半部分将暂时搁置一段时间;