Vuex学习笔记

138 阅读10分钟

vuex是一个专门为vue.js设计的集中式状态管理架构。我把他理解为在data中的属性需要共享给其他vue组件使用的部分。

vuex插件导出一个对象,包含install方法、Store类。在使用时第一步先install安装vuex插件,第二步配置options实例化Store类,并导出此store实例,将store实例添加到根组件的选项中。

install方法到底做了什么?

vuex插件中的install方法,做了很简单一件事,就是混入(mixin)vue生命周期的beforeCreate钩子函数。

beforeCreate钩子函数中做了什么?也很简单就是将注册到根组件中的store添加到所有的子组件中,使得所有的子组件可以共享这一个store实例(这也就是所谓的集中式状态管理)

Vue.mixin({
    beforeCreate: vuexInit
  });

function vuexInit() {
  const options = this.$options;
  //   使得所有所有vue实例都共享 $store 这个对象
  if (options.store) {
    //   根实例
    this.$store = options.store;
  } else if (options.parent && options.parent.$store) {
    //   子组件
    this.$store = options.parent.$store;
  }
}

Store类的实现

在说实现之前,我们看一下Store的options都包含哪一些我们常用的。

export default new Vuex.Store({
  strict: true, // 使用严格模式  只能通过mutation改数据,且mutation只能是同步代码
  // 使用插件
	plugins: [persists, logger()],
  //  组件的状态 类似于 data
  state: {
    age: 10,
    num: 1
  },
  // 获取 计算属性 computed
  getters: {
    getAge(state) {
      // 需要传入 state
      return state.age + 10;
    }
  },
  // vue中的方法 唯一可以改变状态方法
  // 同步的
  mutations: {
    // payload 是载荷的意思
    changeAge(state, payload) {
      state.age += payload;
    }
  },
  // 异步修改state,当中也会调用mutations的同步方法
  actions: {
    asyncChangeAge({ commit }, payload) {
      setTimeout(() => {
        // changeAge是mutations中的方法
        commit("changeAge", payload);
      }, 1000);
    }
  },
  modules: {
    a: {
      namespaced: true,
      state: {
        m_a_age: 20
      },
      mutations: {
        changeAge(state) {
          console.log("a子模块中的changeAge触发!");
          return (state.m_a_age += 2);
        }
      }
    },
    b: {
      namespaced: true,
      state: {
        m_b_age: 30
      },
      getters: {
        getD(state) {
          return state.m_b_age + 10;
        }
      },
      mutations: {
        changeAge(state) {
          console.log("b子模块中的changeAge触发!");
          return (state.m_b_age += 5);
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            m_b_c_age: 40
          },
          mutations: {
            changeAge(state) {
              console.log("子模块b>c中的changeAge触发!");
              return (state.m_b_c_age += 7);
            }
          }
        }
      }
    },
  }
});
/* 
1.默认模块没有  作用域问题,如果需要作用域需要 设置namespaced:true

2.状态不要和模块的名字相同,如果相同 模块优先,会覆盖状态
 原本想取值$store.state.b.m_b_age ,此时如果b的modules 中有一个子模块 也叫 m_b_age 那么就会优先区这个歌子模块,而取不到这个state

 3.默认计算属性 直接通过getters取值 不需要添加模块
$store.getters.b.getD  这个是错误的   
$store.getters.getD 这是正确的
但是如果该模块或该模块的父级配置了namespaced 就需要如下取值:
 b子模块中的getters获取:{{ $store.getters["b/getD"] }}

 4.默认会找当前模块上是否有namespace,并且将父级的namespace 一同算上做成命名空间
例如:
c模块本身有namespaced:true  父模块b本身也有namespaced:true 则在调用时,需要调用'b/c/属性'
c模块本身没有,父模块有,则在调用时,需要调用'b/属性'
c模块本身有,父模块没有,则在调用时,需要调用'c/属性'
*/

常用的options主要有:

  • strict

    在严格模式下,state的改变只能使用mutations,且mutauions只能是同步

  • plugins

    配置相关插件,在实例化store时会调用插件列表

  • namespaced

    配置命名空间

  • state

    与其他组件共享的状态

  • getters

    类似于计算属性

  • mutations

    同步方式,唯一可以修改状态的API,用户通过调用commit触发,这是基于订阅发布的方式

  • actions

    异步方式,actions里面放一些异步逻辑用来修改状态,用户通过调用dispatch触发,在action内部也会调用commit,这也是基于订阅发布的方式(流程:用户调用dispatch触发对应action,执行完异步逻辑后,action会调用commit触发对应mutation修改数据,所以mutation是唯一的修改入口)

  • modules

    可以在根模块下,配置相应的子模块

一、格式化options选项

将用户传入的options实例化为一个ModuleCollection对象

//  格式化 用户传入的参数,格式化成树形结构 更直观一些,后续也更好操作一些
this._modules = new ModuleCollection(options);
class ModuleCollection {
  constructor(options) {
    //  注册模块  第一个参数是一个[]数组,第二个参数是用户传入的options
    this.register([], options);
  }
  register(path, rootModule) {
    // 第一次register时 ,path =[] ,rootModule = options 就是用户传入的选项 实例化Module对象
    // 实例化Module对象后,此时newModule对象就拥有 _rawModule(用户的原始模块配置)_children(子模块)state(当前模块的共享状态)
    let newModule = new Module(rootModule);
    // 在原始用户配置添加 newModule属性,值为对应的module对象
    rootModule.newModule = newModule;
    // 最开始 path是一个[]
    if (path.length === 0) {
      //  将此newModule 赋值给 this.root 此时的this是ModuleCollection的实例
      this.root = newModule;
    } else {
      // 第二次调用时  此时path=["a"]  path.slice(0, -1)后就是一个空数组,所以直接返回根module
      //     查找父亲 this.root就是根module 第一次memo值为根module
      let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo.getChild(current);
      }, this.root);
      // 此时 this.root中的_children中就有一个a module
      // 此时 this.root中的_children中就有一个b module
      //  第三次递归的时候 ,parent 就是this.root._children[b] ,所以会将c module添加到 this.root._children[b]中
      // 在父亲的_children属性中添加newModule
      parent.addChild([path[path.length - 1]], newModule);
    }
    //     如果rootModule中还有 modules属性,说明此模块有子模块,需要递归
    if (rootModule.modules) {
      // forEach是自定义的方法 forEach(对象,fn(value,key))
      forEach(rootModule.modules, (module, moduleName) => {
        // 这里的module是用户配置的子模块 , moduleName是配置的模块名
        // 递归调用  第一个参数数组,始终保存着当前module的父级,可以这么理解
        // 按照以上options,第一次调用时 path为[],moduleName为‘a’,module是用户的手动配置
        this.register([...path, moduleName], module);
      });
    }
  }
  
// 递归结束后,会形成以下树形结构:并赋值给this._modules 注意这里的this指的是store对象
// this.root = {
//   _raw: xxx, // _raw 是指 原始的 用户配置
//   state: xxx.state,
//   _children: {
//     a: {
//       _raw: yyy,
//       state: yyy.state
//     },
//     b: {
//       _raw: ooo,
//       state: ooo.state,
//       _children: {
//         c: {
//           _raw: sss,
//           state: sss.state
//         }
//       }
//     }
//   }
// };
class Module {
  constructor(rootModule) {
      // 接收传入的rootModule ,第一次是 options 也是根模块,后面全是子模块,赋值给module对象的_rawModule属性
    this._rawModule = rootModule;
    // 初始化_children属性为一个空对象
    this._children = {};
    // 将用户传入的模块state赋值给module对象的state属性,方便获取
    this.state = rootModule.state;
  }
  // 定义 获取孩子的方法,返回的也是module对象
  getChild(key) {
    return this._children[key];
  }
  // 定义 添加孩子的方法
  addChild(key, Module) {
    this._children[key] = Module;
  }
  ...
}

二、将格式化后的modules安装到store中

这个阶段的目的是,将modules添加到store的属性上(可以这么理解)。此时的modules是一个树形结构的对象,拥有_rawModule、__children、state属性。

installModule()方法调用结束后,此时 this._mutations 、 this._actions 、 this._wrappedGetters中都会有数据了,且根state中也保存子state。

constructor(options) {
   // ...
  let state = this._modules.root.state; // 根的状态
  this._mutations = {}; // 存放所有模块中的mutation
  this._actions = {}; // 存放所有模块中的actions
  this._wrappedGetters = {}; // 存放所有模块中的getters
  // 第一个参数 this:当前store实例
  // 第二个参数 state:根state
  // 第三个参数 []
  // 第四个参数 根module
	installModule(this, state, [], this._modules.root);
}
  // 第一个参数 传入的store实例
  // 第二个参数 根state
  // 第三个参数 path=[]
  // 第四个参数 根module
function installModule(store, rootState, path, module) {
	// getNameSpace返回的是类似于 ‘a/b/c/’的字符串
  let namespaced = store._modules.getNameSpace(path);
  // 如果path的长度大于0,说明根模块有child
  // path = ["a"]    深度 1级
  // path = ['b','c']   深度 2级
  // path = ['d','e','f'] 深度 3级
  if (path.length > 0) {
    let parent = path.slice(0, -1).reduce((memo, current) => {
      // 第一次是[a],然后 被slice(0,-1)截取后是一个[]数组,直接返回根的state并赋值给parent变量,所以会在根的state中添加a属性,值为a模块的state
      // 第二次是[b],然后 被slice(0,-1)截取后是一个[]数组,直接返回根的state并赋值给parent变量,所以会在根的state中添加b属性,值为b模块的state
      // 第三次是[b,c]然后 被slice(0,-1)截取后[b],此时返回state.b并赋值给parent变量,然后在b模块的state中添加c属性,值为c模块的state
      return memo[current];
    }, rootState);
    // _withCommitting 的作用是确保 用户是mutation调用的,与strict有关,后面会说,主要关注 Vue.set
    store._withCommitting(() => {
      // parent 在深度一级的情况下就 根state
      Vue.set(parent, path[path.length - 1], module.state);
    });
    // child遍历结束后,rootState中会有所有子模块的state 需要通过. 的方式去获取
    // 例如 :$store.state.属性名  $store.state.模块名.属性名
  }
  // 下面的forEachMutation、forEachAction、forEachGetters、forEachChild都是在module中定义的,查看第二段代码
  module.forEachMutation((mutation, type) => {
    // 先看 store._mutations是一个对象,键 为 namespaced + type ,值为数组,数组里面是一个个mutation
    // 所以如果不定义namespaced的话,所有模块的mutation都会被放在一起,然后会一起被调用
    store._mutations[namespaced + type] =
      store._mutations[namespaced + type] || [];
    //  将所有模块的mutation外面再包装一个函数并 类似于函数切片  添加到 store._mutataions中
    // 将当前模块的mutations都订阅到store._mutations[namespaced + type]数组中,等用户commit时依次触发
    store._mutations[namespaced + type].push(payload => {
      // 内部可能会替换新的状态(replaceState方法 暂时还没有说道),这里如果一直使用module.state 可能是老的状态   闭包的问题,这里的module是installMdoule参数中传入的module 所以使用getState(store, path)  是获取最新的state 
      store._withCommitting(() => {
        mutation.call(store, getState(store, path), payload);
      });
      
      // 先不用看,谢谢
      store._subscribers.forEach(sub => {
        return sub({ mutation, type }, store.state);
      });
    });
  });
  module.forEachAction((action, type) => {
    // 类似于 mutations
    store._actions[namespaced + type] = store._actions[namespaced + type] || [];
    store._actions[namespaced + type].push(payload => {
      action.call(store, store, payload);
    });
  });
  module.forEachGetters((getter, key) => {
    // 注意  同名的会被覆盖,因为getter 会被放在同一个对象中
    store._wrappedGetters[namespaced + key] = function() {
      // getState()获取最新的state
			return getter(getState(store, path));
    };
  });
  module.forEachChild((child, key) => {
    // 如果有孩子就继续递归
    // store 还是原来的stroe 没有改变
    // rootState 还是原来的根state
    // 如果 将子module的key添加到path中 ,child是子module
    installModule(store, rootState, path.concat(key), child);
  });
}
class ModuleCollection {  
  // ... 
  getNameSpace(path) {
    // 获取根module
    let root = this.root;
    return path.reduce((namespace, key) => {
      // 如果 path =['a','b','c']
      // root = root._children['a']  然后判断有没有 nameSpced (注意这里的nameSpaced是不是用户写的,是Module类中的属性访问器,只不过恰巧同名了),有的话 就拼接 ‘a/’
      // root = root._children['a']._children['b'] 然后判断有没有 nameSpced 有的话 就拼接 ‘a/b/’
      // root = root._children['a']._children['b']._children['c'] 然后判断有没有 nameSpced 有的话 就拼接 ‘a/b/c/’
      // 所以 getNameSpace方法最后返回的是 类似于‘a/b/c/’的字符串
      root = root.getChild(key);
      return namespace + (root.nameSpaced ? key + "/" : "");
    }, "");
  }
 }
class Module {
 // ...
  forEachMutation(fn) {
    if (this._rawModule.mutations) {
      forEach(this._rawModule.mutations, fn);
    }
  }
  forEachAction(fn) {
    if (this._rawModule.actions) {
      forEach(this._rawModule.actions, fn);
    }
  }
  forEachGetters(fn) {
    if (this._rawModule.getters) {
      forEach(this._rawModule.getters, fn);
    }
  }
  forEachChild(fn) {
    forEach(this._children, fn);
  }
}

三、状态转换响应式

经过第二个步骤后,此时所有的状态都已经存在于state属性中了,就下来我们就需要将state转换为响应式的。

class Store {
  constructor(options) {
   // ...
    // this是当前store实例 ,state是根state
    resetStoreVm(this, state);
   // ...
  }
}
function resetStoreVm(store, state) {
  // store._vm一开始是没有的,这里保存 oldVm 是在调用repalceState方法时,需要销毁旧的vm实例
  let oldVm = store._vm;
  // 第一步获取 getters的集合对象
  let _wrappedGetters = store._wrappedGetters;
  // 第二步 定义computed对象  等下添加到vm中  计算属性
  let computed = {};
  // 在 store实例上,定义一个getters属性 用于用户使用
  store.getters = {};
  // 遍历 _wrappedGetters对象,并定义到computed中
  forEach(_wrappedGetters, (fn, key) => {
    computed[key] = function() {
      return fn(store.state);
    };
    // 并在store.getters中定义
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key]
    });
  });
  store._vm = new Vue({
    data() {
      return {
        $$state: state
      };
    },
    computed
  });
  // 如果用户使用了 strict 严格模式,需要对 store._vm._data.$$state 深度监听,且同步执行
  if (store.strict) {
    store._vm.$watch(
      // 必须函数返回一个对象
      () => store._vm._data.$$state,
      () => {
        // 只要状态一变化就会立即执行
        // console.assert(exp,msg)  exp必须返回boolean  在exp为false时,输出 msg
        console.assert(store._committing, "在mutation外改变了状态");
      },
      {
        deep: true,
        sync: true // 同步执行
      }
    );
  }
  // 如果存在oldVm则进行销毁
  if (oldVm) {
    Vue.nextTick(() => {
      oldVm.$destroyed();
    });
  }
}

至此的话,Store类的constructor都执行完毕了。

_withCommitting是干嘛的?

通俗点说,就是在调用mutation修改state之前,将store._committing属性设置为true,然后在mutation执行完毕后,再将stroe._committing设置为false。

为什么要这要做呢?因为用户用户设置了strict:true,在严格模式下,只能通过mutation去修改属性,且mutation是同步。不能通过store.state.key=value的方式去执行,也不能在mutation使用异步方式。实现原理就,通过store.vm.store.state.key = value的方式去执行,也不能在mutation使用异步方式。实现原理就,通过store._vm.watch观察根state,只要观察到根state有变化,就会执行回调函数

() => {
        // 只要状态一变化就会立即执行
        // console.assert(exp,msg)  exp必须返回boolean  在exp为false时,输出 msg
        console.assert(store._committing, "在mutation外改变了状态");
},

subscribe订阅

将fn订阅到store._subscribers中,每当commit时,会执行对应的mutation然后依次调用store._subscribers中的fn。

注意:一定是通过commit的方式触发的mutation才会执行fn

replaceState替换State

为什么会替换配置好的State?可以用来持久化本地存储。

replaceState(newState) {
    this._withCommitting(() => {
      // 一定要用_withCommitting包裹,不然在严格模式下会报错
      // 数据改变,会触发当前组件实例视图更新
      this._vm._data.$$state = newState;
    });
  }

registerModule动态注册模块

registerModule(path, rawModule) {
    // 将path转换为数组
    if (typeof path == "string") {
      path = [path];
    }
    // 模块注册 ['u'] 将u注册到根module下
    this._modules.register(path, rawModule);
    // 模块安装
    installModule(this, this.state, path, rawModule.newModule);
    //  重新定义getters
    resetStoreVm(this, this.state);
  }