【原理浅析】Vuex

1,215 阅读1分钟

介绍

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 这种集中管理应用状态的模式相比父子组件通信来说,使数据的通信更方便,状态的更改也更加直观。

Bus

肯定有不少同学在写 Vue 时使用过 new Vue() 创建 bus 进行数据通信。

import Vue from 'vue';
const bus = new Vue();
export default {
  install(Vue) {
    Object.defineProperty(Vue.prototype, '$bus', {
      get () { return bus }
    });
  }
};

组件中使用 this.$bus.$on this.$bus.$emit 监听和触发 bus 事件进行通信。 bus 的通信是不依赖组件的父子关系的,因此实际上可以理解为最简单的一种状态管理模式。 通过 new Vue() 可以注册响应式的数据, 下面基于此对 bus 进行改造,实现一个最基本的状态管理:

// /src/vuex/bus.js
let Vue
// 导出一个 Store 类,一个 install 方法
class Store {
  constructor (options) {
    // 将 options.state 注册为响应式数据
    this._bus = new Vue({
      data: {
        state: options.state
      }
    })
  }
  // 定义 state 属性
  get state() {
    return this._bus._data.state;
  }
}
function install (_Vue) {
  Vue = _Vue
  // 全局混入 beforeCreate 钩子
  Vue.mixin({
    beforeCreate () {
      // 存在 $options.store 则为根组件
      if (this.$options.store) {
        // $options.store 就是创建根组件时传入的 store 实例,直接挂在 vue 原型对象上
        Vue.prototype.$store = this.$options.store
      }
    }
  })
}
export default {
  Store,
  install
}

创建并导出 store 实例:

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex/bus'
Vue.use(Vuex) // 调用 Vuex.install 方法
export default new Vuex.Store({
  state: {
    count: 0
  }
})

创建根组件并传入 store 实例:

// /src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

组件中使用示例:

<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
  </div>
</template>
<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.state.count++
    }
  }
}
</script>

从零实现一个 Vuex

前一节通过 new Vue() 定义一个响应式属性并通过 minxin 为所有组件混入 beforeCreate 生命周期钩子函数的方法为每个组件内添加 $store 属性指向根组件的 store 实例的方式,实现了最基本的状态管理。 继续这个思路,下面从零一步步实现一个最基本的 Vuex。

以下代码的 git 地址:simple-vuex

整体结构

let Vue;
class Store {}
function install() {}
export default {
  Store,
  install
}

install 函数

// 执行 Vue.use(Vuex) 时调用 并传入 Vue 类
// 作用是为所有 vue 组件内部添加 `$store` 属性
function install(_Vue) {
  // 避免重复安装
  if (Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error('[vuex] already installed. Vue.use(Vuex) should be called only once.');
    }
    return
  }
  Vue = _Vue; // 暂存 Vue 用于其他地方有用到 Vue 上的方法
  Vue.mixin({
    // 全局所有组件混入 beforeCreate 钩子,给每个组件中添加 $store 属性指向 store 实例
    beforeCreate: function vuexInit() {
      const options = this.$options;
      if (options.store) {
        // 接收参数有=中有 store 属性则为根组件
        this.$store = options.store;
      } else if (options.parent && options.parent.$store) {
        // 非根组件通过 parent 父组件获取
        this.$store = options.parent.$store;
      }
    }
  })
}

Store 类

// 执行 new Vuex.Store({}) 时调用
class Store {
  constructor(options = {}) {
    // 初始化 getters mutations actions
    this.getters = {};
    this._mutations = {};
    this._actions = {};
    // 给每个 module 注册 _children 属性指向子 module
    // 用于后面 installModule 中根据 _children 属性查找子 module 进行递归处理
    this._modules = new ModuleCollection(options)
    const { dispatch, commit } = this;
    // 固定 commit dispatch 的 this 指向 Store 实例
    this.commit = (type, payload) => {
      return commit.call(this, type, payload);
    }
    this.dispatch = (type, payload) => {
      return dispatch.call(this, type, payload);
    }
    // 通过 new Vue 定义响应式 state
    const state = options.state;
    this._vm = new Vue({
      data: {
        state: state
      }
    });
    // 注册 getters  mutations actions
    // 并根据 _children 属性对子 module 递归执行 installModule
    installModule(this, state, [], this._modules.root);
  }
  // 定义 state commit dispatch
  get state() {
    return this._vm._data.state;
  }
  set state(v){
    throw new Error('[Vuex] vuex root state is read only.')
  }
  commit(type, payload) {
    return this._mutations[type].forEach(handler => handler(payload));
  }
  dispatch(type, payload) {
    return this._actions[type].forEach(handler => handler(payload));
  }
}

ModuleCollection 类

Store 类的构造函数中初始化 _modules 时是通过调用 ModuleCollection 这个类,内部从根模块开始递归遍历 modules 属性,初始化模块的 _children 属性指向子模块。

class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule)
  }
  // 递归注册,path 是记录 module 的数组 初始为 []
  register(path, rawModule) {
    const newModule = {
      _children: {},
      _rawModule: rawModule,
      state: rawModule.state
    }
    if (path.length === 0) {
      this.root = newModule;
    } else {
      // 非最外层路由通过 reduce 从 this.root 开始遍历找到父级路由
      const parent = path.slice(0, -1).reduce((module, key) => {
        return module._children[key];
      }, this.root);
      // 给父级路由添加 _children 属性指向该路由
      parent._children[path[path.length - 1]] = newModule;
      // 父级路由 state 中也添加该路由的 state
      Vue.set(parent.state, path[path.length - 1], newModule.state);
    }
    // 如果当前 module 还有 module 属性则遍历该属性并拼接 path 进行递归
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule);
      })
    }
  }
}

installModule

Store 类的构造函数中调用 installModule ,通过 _modules 的 _children 属性遍历到每个模块并注册 getters mutations actions

function installModule(store, rootState, path, module) {
  if (path.length > 0) {
    const parentState = rootState;
    const moduleName = path[path.length - 1];
    // 所有子模块都将 state 添加到根模块的 state 上
    Vue.set(parentState, moduleName, module.state)
  }
  const context = {
    dispatch: store.dispatch,
    commit: store.commit,
  }
  // 注册 getters mutations actions
  const local = Object.defineProperties(context, {
    getters: {
      get: () => store.getters
    },
    state: {
      get: () => {
        let state = store.state;
        return path.length ? path.reduce((state, key) => state[key], state) : state
      }
    }
  })
  if (module._rawModule.actions) {
    forEachValue(module._rawModule.actions, (actionFn, actionName) => {
      registerAction(store, actionName, actionFn, local);
    });
  }
  if (module._rawModule.getters) {
    forEachValue(module._rawModule.getters, (getterFn, getterName) => {
      registerGetter(store, getterName, getterFn, local);
    });
  }
  if (module._rawModule.mutations) {
    forEachValue(module._rawModule.mutations, (mutationFn, mutationName) => {
      registerMutation(store, mutationName, mutationFn, local)
    });
  }
  // 根据 _children 拼接 path 并递归遍历
  forEachValue(module._children, (child, key) => {
    installModule(store, rootState, path.concat(key), child)
  })
}

installModule 中用来注册 getters mutations actions 的函数:

// 给 store 实例的 _mutations 属性填充
function registerMutation(store, mutationName, mutationFn, local) {
  const entry = store._mutations[mutationName] || (store._mutations[mutationName] = []);
  entry.push((payload) => {
    mutationFn.call(store, local.state, payload);
  });
}

// 给 store 实例的 _actions 属性填充
function registerAction(store, actionName, actionFn, local) {
  const entry = store._actions[actionName] || (store._actions[actionName] = [])
  entry.push((payload) => {
    return actionFn.call(store, {
      commit: local.commit,
      state: local.state,
    }, payload)
  });
}

// 给 store 实例的 getters 属性填充
function registerGetter(store, getterName, getterFn, local) {
  Object.defineProperty(store.getters, getterName, {
    get: () => {
      return getterFn(
        local.state,
        local.getters,
        store.state
      )
    }
  })
}

// 将对象中的每一个值放入到传入的函数中作为参数执行
function forEachValue(obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key));
}

使用

还有 modules、plugins 等功能还没有实现,而且 getters 的并没有使用 Vue 的 computed 而只是简单的以函数的形式实现,但是已经基本完成了 Vuex 的主要功能,下面是一个使用示例:

// /src/store.js
import Vue from 'vue'
import Vuex from './vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    changeCount(state, payload) {
      console.log('changeCount', payload)
      state.count += payload;
    }
  },
  actions: {
    asyncChangeCount(ctx, payload) {
      console.log('asyncChangeCount', payload)
      setTimeout(() => {
        ctx.commit('changeCount', payload);
      }, 500);
    }
  }
})
<!-- /src/App.vue -->
<template>
  <div id="app">
    {{ count }}
    <button @click="changeCount">+1</button>
    <button @click="asyncChangeCount">async +1</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    changeCount() {
      this.$store.commit('changeCount', 1);
    },
    asyncChangeCount() {
      this.$store.dispatch('asyncChangeCount', 1);
    }
  },
  mounted() {
    console.log(this.$store)
  }
}
</script>

阅读源码的过程中写了一些方便理解的注释,希望给大家阅读源码带来帮助,github: vuex 源码

参考