稍微学一下 Vuex 原理

作者: ST_Pace | 来源:发表于2019-08-26 10:37 被阅读0次
    vue.jpg

    博客原文

    介绍

    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 源码

    参考

    相关文章

      网友评论

        本文标题:稍微学一下 Vuex 原理

        本文链接:https://www.haomeiwen.com/subject/icwlectx.html