学习Vuex实现原理

390 阅读3分钟

Vuex 的使用用例

const store = createStore({
  state: {},
  mutations: {},
  actions: {},
  getters: {},
  modules: {
    childModule1: {
      namespaced: true,
      state: {},
      mutations: {},
      actions: {},
      getters: {},
      modules: {
        grandsonModule: {
          namespaced: true,
          state: {},
          mutations: {},
          actions: {},
          getters: {},
        },
      },
    },
    childModule2: {
      namespaced: true,
      state: {},
      mutations: {},
      actions: {},
      getters: {},
    },
  },
});

在使用 Vuex 的过程中,经常会用到多个模块。那么 Vuex 是怎么注册和处理这些模块的呢?

modules 的注册与收集

class Store {
  constructor(options) {
    const store = this;

    // 收集模块
    store._modules = new ModuleCollection(options);
  }
}

vuex模块收集.png

ModuleCollection

该类主要是用来处理嵌套的 modules,将用户的模块转化为 Vuex 内部的树结构

class ModuleCollection {
  constructor(rootModule) {
    this.root = null;
    this.register(rootModule, []);
  }

  register(rawModule, path) {
    const newModule = new Module(rawModule);

    if (path.length == 0) {
      // 表示为一个根模块
      this.root = rawModule;
    } else {
      // 获取path中除了最后一项的前面几项,从root开始获取child
      // 因为最后一项是代表当前的module
      const parent = path.slice(0, -1).reduce((module, cur) => {
        return module.getChild(cur);
      }, this.root);

      // 将当前的module挂载到path路径对应的module上
      parent.addChild(path[path.length - 1], newModule);
    }

    // 如果该模块含有子模块,注册其子模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 注册子模块,并将模块的路径(模块的key)拼接上去
        this.register(rawChildModule, path.concat(key));
      });
    }
  }
}

Module 的实现

class Module {
  constructor(rawModule) {
    this._raw = rawModule;
    this._children = {};
    this.state = rawModule.state;
    this.namespaced = rawModule.namespaced;
  }

  getChild(key) {
    return this._children(key);
  }

  addChild(key, module) {
    this._children[key] = module;
  }

  forEachChild(fn) {
    forEachValue(this._children, fn);
  }

  forEachGetter(fn) {
    if (this._raw.getters) {
      forEachValue(this._raw.getters.fn);
    }
  }

  forEachMutation(fn) {
    if (this._raw.mutations) {
      forEachValue(this._raw.mutations, fn);
    }
  }

  forEachAction(fn) {
    if (this._raw.actions) {
      forEachValue(this._raw.actions, fn);
    }
  }
}

经过处理,module最终会变成下面这样的数据结构

store: {
  _modules: {
    root: {
      _raw: rootModule,
      state: rootModule.state, 
      _children: {
        childModule1: {
          _raw: childModule1,
          state: childModule1.state,
          _children: {
            grandsonModule: {
              _raw: childModule2,
              state: grandsonModule.state,
              _children: {},
            },
          },
        },
        childModule2: {
          _raw: childModule2,
          state: childModule2.state,
          _children: {},
        },
      },
    }
  }
}

处理modules的 getters, mutations, actions

将所有模块的getters, mutations, actions全部注册在store实例上

store._wrappedGetters = Object.create(null);
store._mutations = Object.create(null);
store._actions = Object.create(null);

// 定义状态
const state = store._modules.root.state; // 根状态
// 
installModule(store, state, [], store._modules.root);

function getNestedState(state, path){
  return path.reduce((curState, key) => curState[key], state)
}

function installModule(store, state, path, module){

  const isRoot = !path.length;

  if(!isRoot){

  }

  // {double: function(state){return state.count * 2}}
  module.forEachGetter((getter, key)=>{
    store._wrappedGetters[key] = () => {
      return getter(getNestedState(store.state, path))
    }
  })

  // mutation是基于发布订阅模式的,可能会有多个回调函数
  // {add: [mutation, mutation, ...]}
  module.forEachMutation((mutation, key) => {
      const entry = store._mutations[key] || (store._mutations[key] = [])
      entry.push((payload) => {  // store.commit("add", payload)
          mutation.call(store, getNestedState(store.state, path), payload)
      })
  })

  // actions mutation和action的一个区别, action执行后返回一个是promise 
  module.forEachAction((action, key) => {
      const entry = store._actions[key] || (store._actions[key] = [])
      entry.push((payload) => {
          let res = action.call(store, store, payload)
          // 判断res是否为一个promise
          if (!isPromise(res)) {
              return Promise.resolve(res)
          }
          return res
      })
  })

  // 如果有子模块,安装子模块
  module.forEachChild((child, key) => {
      installModule(store, rootState, path.concat(key), child)
  })

}

命名空间的处理

通过namespaced来标识是否开启命名空间

ModuleCollection 的类中添加一个获取命名空间的方法getNamespaced

// [a,c] => a/c
getNamespaced(path){
    let module = this.root;
    return path.reduce((nameSpaceStr, key)=>{ 
        module = module.getChild(key);  // 获取子模块
        return nameSpaceStr + (module.namespaced ? key + "/" : "")
    },"")
}

在处理getters, mutations, actions时候,将命名空间添加上

const namespaced = store._modules.getNamespaced(path)

// store.getters["some/nested/module/foo"]
module.forEachGetter((getter, key) => {  
    store._wrappedGetters[namespaced + key] = () => {
      // ...
    }
})

// store.commit("some/nested/module/foo", payload)
module.forEachMutation((mutation, key) => {
    const entry = store._mutations[namespaced + key] || (store._mutations[namespaced + key] = [])
    // ...
})

// store.dispatch("some/nested/module/foo", payload)
module.forEachAction((action, key) => {
    const entry = store._actions[namespaced + key] || (store._actions[namespaced + key] = [])
    // ...
})

resetStoreState

代理getter,并处理state的响应式

function resetStoreState(store, state){
  // 用data包裹一层是为了修改的时候方便 store._state.date = 'xxx' 不会影响数据的响应式
  store._state = reactive({data: state})

  const wrappedGetters = store._wrappedGetters;
  store.getters = {};
  forEachValue(wrappedGetters, (getter, key) => {
      Object.defineProperty(store.getters, key, {
          get: getter,
          enumerable: true
      })
  })
}

开启严格模式

在options中可以指定是否开始严格模式,开启之后,只能在commit时候修改state,其他的修改都是不合法,会报错。

严格模式实现基本思路

  • 在mutation之前添加一个状态,_commiting = true
  • 调用mutation => 更改状态, 监控这个状态,如果当前状态变化的时候,_commiting = true,同步修改
  • 修改完毕后 _commiting = false
  • 如果_commiting = false,状态发生了改变,则为非法修改
class Store{
  this.strict = options.strict || false;
  this._commiting = false;

  resetStoreState(store, state)

  _withCommit(fn){ // 切片模式编程
      const commiting = this._commiting;
      this._commiting = true;
      // 只有在fn中修改状态才是合法的操作
      fn();
      this._commiting = commiting;
  }

  commit = (type, payload) => {
      const entry = this._mutations[type] || [];
      this._withCommit(()=>{
          entry.forEach(handler => handler(payload))
      })
  }
  dispatch = (type, payload) => {
      const entry = this._actions[type] || []
      return Promise.all(entry.map(handler => handler(payload)))
  }
}

function resetStoreState(store, state){
  if(store.strict){
    enableStrictMode(store)
  }
}

function enableStictMode(store){
  watch(()=>store._state.data, ()=>{
    console.assert(store._commiting, "不能在mutation之外修改state")
  }, {deep: true, flush: 'sync'})
}

useStore

export const storeKey = 'store'
import {inject} from 'vue'
 // createApp().use(store, 'my')
class Store{
  install(app, injectKey) { 
      // 全局暴露一个 变量,暴露的是store的实例
      app.provide(injectKey || storeKey, this)

      // Vue2.x Vue.prototype.$store = this
      app.config.globalProperties.$store = this; // 增添$store属性 使得可以直接在模板中使用 $store.state.count

  }
}

// vue 内部已经将这个些api导出来了
// const store = useStore("my")
export function useStore(injectKey = null) {
    return inject(injectKey !== null ? injectKey : storeKey)
}