博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vuex源码解析
阅读量:4085 次
发布时间:2019-05-25

本文共 18294 字,大约阅读时间需要 60 分钟。

vuex简介

能看到此文章的人,应该大部分都已经使用过vuex了,想更深一步了解vuex的内部实现原理。所以简介就少介绍一点。官网介绍说Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。数据流的状态非常清晰,按照 组件dispatch Action -> action内部commit Mutation -> Mutation再 mutate state 的数据,在触发render函数引起视图的更新。附上一张官网的流程图及vuex的官网地址:

 

 

Questions

在使用vuex的时候,大家有没有如下几个疑问,带着这几个疑问,再去看源码,从中找到解答,这样对vuex的理解可以加深一些。

  1. 官网在严格模式下有说明:在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。vuex是如何检测状态改变是由mutation函数引起的?
  2. 通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中。为什么所有子组件都可以取到store?
  3. 为什么用到的属性在state中也必须要提前定义好,vue视图才可以响应?
  4. 在调用dispatch和commit时,只需传入(type, payload),为什么action函数和mutation函数能够在第一个参数中解构出来state、commit等? 带着这些问题,我们来看看vuex的源码,从中寻找到答案。

源码目录结构

vuex的源码结构非常简洁清晰,代码量也不是很大,大家不要感到恐慌。

 

 

 

vuex挂载

vue使用插件的方法很简单,只需Vue.use(Plugins),对于vuex,只需要Vue.use(Vuex)即可。在use 的内部是如何实现插件的注册呢?读过vue源码的都知道,如果传入的参数有 install 方法,则调用插件的 install 方法,如果传入的参数本身是一个function,则直接执行。那么我们接下来就需要去 vuex 暴露出来的 install 方法去看看具体干了什么。

store.js

export function install(_Vue) {  // vue.use原理:调用插件的install方法进行插件注册,并向install方法传递Vue对象作为第一个参数  if (Vue && _Vue === Vue) {    if (process.env.NODE_ENV !== "production") {      console.error(        "[vuex] already installed. Vue.use(Vuex) should be called only once."      );    }    return;  }  Vue = _Vue; // 为了引用vue的watch方法  applyMixin(Vue);}复制代码

在 install 中,将 vue 对象赋给了全局变量 Vue,并作为参数传给了 applyMixin 方法。那么在 applyMixin 方法中干了什么呢?

mixin.js

function vuexInit() {    const options = this.$options;    // store injection    if (options.store) {      this.$store =        typeof options.store === "function" ? options.store() : options.store;    } else if (options.parent && options.parent.$store) {      this.$store = options.parent.$store;    }  }复制代码

在这里首先检查了一下 vue 的版本,2以上的版本把 vuexInit 函数混入 vuex 的 beforeCreate 钩子函数中。 在 vuexInit 中,将 new Vue() 时传入的 store 设置到 this 对象的 $store 属性上,子组件则从其父组件上引用其 $store 属性进行层层嵌套设置,保证每一个组件中都可以通过 this.$store 取到 store 对象。 这也就解答了我们问题 2 中的问题。通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,注入方法是子从父拿,root从options拿。

接下来让我们看看new Vuex.Store()都干了什么。

store构造函数

store对象构建的主要代码都在store.js中,是vuex的核心代码。

首先,在 constructor 中进行了 Vue 的判断,如果没有通过 Vue.use(Vuex) 进行 Vuex 的注册,则调用 install 函数注册。( 通过 script 标签引入时不需要手动调用 Vue.use(Vuex) ) 并在非生产环境进行判断: 必须调用 Vue.use(Vuex) 进行注册,必须支持 Promise,必须用 new 创建 store。

if (!Vue && typeof window !== "undefined" && window.Vue) {    install(window.Vue);}if (process.env.NODE_ENV !== "production") {    assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`);    assert(        typeof Promise !== "undefined",        `vuex requires a Promise polyfill in this browser.`    );    assert(        this instanceof Store,        `store must be called with the new operator.`    );}复制代码

然后进行一系列的属性初始化。其中的重点是 new ModuleCollection(options),这个我们放在后面再讲。先把 constructor 中的代码过完。

const { plugins = [], strict = false } = options;// store internal statethis._committing = false; // 是否在进行提交mutation状态标识this._actions = Object.create(null); // 保存action,_actions里的函数已经是经过包装后的this._actionSubscribers = []; // action订阅函数集合this._mutations = Object.create(null); // 保存mutations,_mutations里的函数已经是经过包装后的this._wrappedGetters = Object.create(null); // 封装后的getters集合对象// Vuex支持store分模块传入,在内部用Module构造函数将传入的options构造成一个Module对象,// 如果没有命名模块,默认绑定在this._modules.root上// ModuleCollection 内部调用 new Module构造函数this._modules = new ModuleCollection(options);this._modulesNamespaceMap = Object.create(null); // 模块命名空间mapthis._subscribers = []; // mutation订阅函数集合this._watcherVM = new Vue(); // Vue组件用于watch监视变化复制代码

属性初始化完毕后,首先从 this 中解构出原型上的 dispatchcommit 方法,并进行二次包装,将 this 指向当前 store。

const store = this;const { dispatch, commit } = this;/** 把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上.  这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时,  能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.*/this.dispatch = function boundDispatch(type, payload) {    return dispatch.call(store, type, payload);};this.commit = function boundCommit(type, payload, options) {    return commit.call(store, type, payload, options);};复制代码

接着往下走,包括严格模式的设置、根state的赋值、模块的注册、state的响应式、插件的注册等等,其中的重点在 installModule 函数中,在这里实现了所有modules的注册。

//options中传入的是否启用严格模式this.strict = strict;// new ModuleCollection 构造出来的_mudulesconst state = this._modules.root.state;// 初始化组件树根组件、注册所有子组件,并将其中所有的getters存储到this._wrappedGetters属性中installModule(this, state, [], this._modules.root);//通过使用vue实例,初始化 store._vm,使state变成可响应的,并且将getters变成计算属性resetStoreVM(this, state);// 注册插件plugins.forEach(plugin => plugin(this));// 调试工具注册const useDevtools =    options.devtools !== undefined ? options.devtools : Vue.config.devtools;if (useDevtools) {   devtoolPlugin(this);}复制代码

到此为止,constructor 中所有的代码已经分析完毕。其中的重点在 new ModuleCollection(options)installModule ,那么接下来我们到它们的内部去看看,究竟都干了些什么。

ModuleCollection

由于 Vuex 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。Vuex 允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。例如下面这样:

const childModule = {  state: { ... },  mutations: { ... },  actions: { ... }}const store = new Vuex.Store({  state,  getters,  actions,  mutations,  modules: {    childModule: childModule,  }})复制代码

有了模块的概念,可以更好的规划我们的代码。对于各个模块公用的数据,我们可以定义一个common store,别的模块用到的话直接通过 modules 的方法引入即可,无需重复的在每一个模块都写一遍相同的代码。这样我们就可以通过 store.state.childModule 拿到childModule中的 state 状态, 对于Module的内部是如何实现的呢?

export default class ModuleCollection {  constructor(rawRootModule) {    // 注册根module,参数是new Vuex.Store时传入的options    this.register([], rawRootModule, false);  }  register(path, rawModule, runtime = true) {    if (process.env.NODE_ENV !== "production") {      assertRawModule(path, rawModule);    }    const newModule = new Module(rawModule, runtime);    if (path.length === 0) {      // 注册根module      this.root = newModule;    } else {      // 注册子module,将子module添加到父module的_children属性上      const parent = this.get(path.slice(0, -1));      parent.addChild(path[path.length - 1], newModule);    }    // 如果当前模块有子modules,循环注册    if (rawModule.modules) {      forEachValue(rawModule.modules, (rawChildModule, key) => {        this.register(path.concat(key), rawChildModule, runtime);      });    }  }}复制代码

在ModuleCollection中又调用了Module构造函数,构造一个Module。

Module构造函数

constructor (rawModule, runtime) {    // 初始化时为false    this.runtime = runtime    // 存储子模块    this._children = Object.create(null)    // 将原来的module存储,以备后续使用    this._rawModule = rawModule    const rawState = rawModule.state    // 存储原来module的state    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}  }复制代码

通过以上代码可以看出,ModuleCollection 主要将传入的 options 对象整个构造为一个 Module 对象,并循环调用 this.register([key], rawModule, false) 为其中的 modules 属性进行模块注册,使其都成为 Module 对象,最后 options 对象被构造成一个完整的 Module 树。

经过 ModuleCollection 构造后的树结构如下:(以上面的例子生成的树结构)

 

 

 

模块已经创建好之后,接下来要做的就是 installModule。

installModule

首先我们来看一看执行完 constructor 中的 installModule 函数后,这棵树的结构如何?

 

 

从上图中可以看出,在执行完installModule函数后,每一个 module 中的 state 属性都增加了 其子 module 中的 state 属性,但此时的 state 还不是响应式的,并且新增加了 context 这个对象。里面包含 dispatch 、 commit 等函数以及 state 、 getters 等属性。它就是 vuex 官方文档中所说的Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 这个 context 对象。我们平时在 store 中调用的 dispatch 和 commit 就是从这里解构出来的。接下来让我们看看 installModule 里面执行了什么。

function installModule(store, rootState, path, module, hot) {  // 判断是否是根节点,跟节点的path = []  const isRoot = !path.length;  // 取命名空间,形式类似'childModule/'  const namespace = store._modules.getNamespace(path);  // 如果namespaced为true,存入_modulesNamespaceMap中  if (module.namespaced) {    store._modulesNamespaceMap[namespace] = module;  }  // 不是根节点,把子组件的每一个state设置到其父级的state属性上  if (!isRoot && !hot) {    // 获取当前组件的父组件state    const parentState = getNestedState(rootState, path.slice(0, -1));    // 获取当前Module的名字    const moduleName = path[path.length - 1];    store._withCommit(() => {      Vue.set(parentState, moduleName, module.state);    });  }  // 给context对象赋值  const local = (module.context = makeLocalContext(store, namespace, path));  // 循环注册每一个module的Mutation  module.forEachMutation((mutation, key) => {    const namespacedType = namespace + key;    registerMutation(store, namespacedType, mutation, local);  });  // 循环注册每一个module的Action  module.forEachAction((action, key) => {    const type = action.root ? key : namespace + key;    const handler = action.handler || action;    registerAction(store, type, handler, local);  });  // 循环注册每一个module的Getter  module.forEachGetter((getter, key) => {    const namespacedType = namespace + key;    registerGetter(store, namespacedType, getter, local);  });  // 循环_childern属性  module.forEachChild((child, key) => {    installModule(store, rootState, path.concat(key), child, hot);  });}复制代码

在installModule函数里,首先判断是否是根节点、是否设置了命名空间。在设置了命名空间的前提下,把 module 存入 store._modulesNamespaceMap 中。在不是跟节点并且不是 hot 的情况下,通过 getNestedState 获取到父级的 state,并获取当前 module 的名字, 用 Vue.set() 方法将当前 module 的 state 挂载到父 state 上。然后调用 makeLocalContext 函数给 module.context 赋值,设置局部的 dispatch、commit方法以及getters和state。那么来看一看这个函数。

function makeLocalContext(store, namespace, path) {  // 是否有命名空间  const noNamespace = namespace === "";  const local = {    // 如果没有命名空间,直接返回store.dispatch;否则给type加上命名空间,类似'childModule/'这种    dispatch: noNamespace      ? store.dispatch      : (_type, _payload, _options) => {          const args = unifyObjectStyle(_type, _payload, _options);          const { payload, options } = args;          let { type } = args;          if (!options || !options.root) {            type = namespace + type;            if (              process.env.NODE_ENV !== "production" &&              !store._actions[type]            ) {              console.error(                `[vuex] unknown local action type: ${                  args.type                }, global type: ${type}`              );              return;            }          }          return store.dispatch(type, payload);        },    // 如果没有命名空间,直接返回store.commit;否则给type加上命名空间    commit: noNamespace      ? store.commit      : (_type, _payload, _options) => {          const args = unifyObjectStyle(_type, _payload, _options);          const { payload, options } = args;          let { type } = args;          if (!options || !options.root) {            type = namespace + type;            if (              process.env.NODE_ENV !== "production" &&              !store._mutations[type]            ) {              console.error(                `[vuex] unknown local mutation type: ${                  args.type                }, global type: ${type}`              );              return;            }          }          store.commit(type, payload, options);        }  };  // getters and state object must be gotten lazily  // because they will be changed by vm update  Object.defineProperties(local, {    getters: {      get: noNamespace        ? () => store.getters        : () => makeLocalGetters(store, namespace)    },    state: {      get: () => getNestedState(store.state, path)    }  });  return local;}复制代码

经过 makeLocalContext 处理的返回值会赋值给 local 变量,这个变量会传递给 registerMutation、forEachAction、registerGetter 函数去进行相应的注册。

mutation可以重复注册,registerMutation 函数将我们传入的 mutation 进行了一次包装,将 state 作为第一个参数传入,因此我们在调用 mutation 的时候可以从第一个参数中取到当前的 state 值。

function registerMutation(store, type, handler, local) {  const entry = store._mutations[type] || (store._mutations[type] = []);  entry.push(function wrappedMutationHandler(payload) {    // 将this指向store,将makeLocalContext返回值中的state作为第一个参数,调用值执行的payload作为第二个参数    // 因此我们调用commit去提交mutation的时候,可以从mutation的第一个参数中取到当前的state值。    handler.call(store, local.state, payload);  });}复制代码

action也可以重复注册。注册 action 的方法与 mutation 相似,registerAction 函数也将我们传入的 action 进行了一次包装。但是 action 中参数会变多,里面包含 dispatch 、commit、local.getters、local.state、rootGetters、rootState,因此可以在一个 action 中 dispatch 另一个 action 或者去 commit 一个 mutation。这里也就解答了问题4中提出的疑问。

function registerAction(store, type, handler, local) {  const entry = store._actions[type] || (store._actions[type] = []);  entry.push(function wrappedActionHandler(payload, cb) {    //与mutation不同,action的第一个参数是一个对象,里面包含dispatch、commit、getters、state、rootGetters、rootState    let res = handler.call(      store,      {        dispatch: local.dispatch,        commit: local.commit,        getters: local.getters,        state: local.state,        rootGetters: store.getters,        rootState: store.state      },      payload,      cb    );    if (!isPromise(res)) {      res = Promise.resolve(res);    }    if (store._devtoolHook) {      return res.catch(err => {        store._devtoolHook.emit("vuex:error", err);        throw err;      });    } else {      return res;    }  });}复制代码

注册 getters,从getters的第一个参数中可以取到local state、local getters、root state、root getters。getters不允许重复注册。

function registerGetter(store, type, rawGetter, local) {  // getters不允许重复  if (store._wrappedGetters[type]) {    if (process.env.NODE_ENV !== "production") {      console.error(`[vuex] duplicate getter key: ${type}`);    }    return;  }  store._wrappedGetters[type] = function wrappedGetter(store) {    // getters的第一个参数包含local state、local getters、root state、root getters    return rawGetter(      local.state, // local state      local.getters, // local getters      store.state, // root state      store.getters // root getters    );  };}复制代码

现在 store 的 _mutation、_action 中已经有了我们自行定义的的 mutation 和 action函数,并且经过了一层内部报装。当我们在组件中执行 this.$store.dispatch()this.$store.commit() 的时候,是如何调用到相应的函数的呢?接下来让我们来看一看 store 上的 dispatch 和 commit 函数。

commit

commit 函数先进行参数的适配处理,然后判断当前 action type 是否存在,如果存在则调用 _withCommit 函数执行相应的 mutation 。

// 提交mutation函数  commit(_type, _payload, _options) {    // check object-style commit    //commit支持两种调用方式,一种是直接commit('getName','vuex'),另一种是commit({type:'getName',name:'vuex'}),    //unifyObjectStyle适配两种方式    const { type, payload, options } = unifyObjectStyle(      _type,      _payload,      _options    );    const mutation = { type, payload };    // 这里的entry取值就是我们在registerMutation函数中push到_mutations中的函数,已经经过处理    const entry = this._mutations[type];    if (!entry) {      if (process.env.NODE_ENV !== "production") {        console.error(`[vuex] unknown mutation type: ${type}`);      }      return;    }    // 专用修改state方法,其他修改state方法均是非法修改,在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误    // 不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。    this._withCommit(() => {      entry.forEach(function commitIterator(handler) {        handler(payload);      });    });    // 订阅者函数遍历执行,传入当前的mutation对象和当前的state    this._subscribers.forEach(sub => sub(mutation, this.state));    if (process.env.NODE_ENV !== "production" && options && options.silent) {      console.warn(        `[vuex] mutation type: ${type}. Silent option has been removed. ` +          "Use the filter functionality in the vue-devtools"      );    }  }复制代码

在 commit 函数中调用了 _withCommit 这个函数, 代码如下。 _withCommit 是一个代理方法,所有触发 mutation 的进行 state 修改的操作都经过它,由此来统一管理监控 state 状态的修改。在严格模式下,会深度监听 state 的变化,如果没有通过 mutation 去修改 state,则会报错。官方建议 不要在发布环境下启用严格模式! 请确保在发布环境下关闭严格模式,以避免性能损失。这里就解答了问题1中的疑问。

_withCommit(fn) {    // 保存之前的提交状态false    const committing = this._committing;    // 进行本次提交,若不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告    this._committing = true;    // 修改state    fn();    // 修改完成,还原本次修改之前的状态false    this._committing = committing;}复制代码

dispatch

dispatch 和 commit 的原理相同。如果有多个同名 action,会等到所有的 action 函数完成后,返回的 Promise 才会执行。

// 触发action函数  dispatch(_type, _payload) {    // check object-style dispatch    const { type, payload } = unifyObjectStyle(_type, _payload);    const action = { type, payload };    const entry = this._actions[type];    if (!entry) {      if (process.env.NODE_ENV !== "production") {        console.error(`[vuex] unknown action type: ${type}`);      }      return;    }    // 执行所有的订阅者函数    this._actionSubscribers.forEach(sub => sub(action, this.state));    return entry.length > 1      ? Promise.all(entry.map(handler => handler(payload)))      : entry[0](payload);  }复制代码

至此,整个 installModule 里涉及到的内容已经分析完毕。现在我们来看一看store树结构。

 

 

我们在 options 中传进来的 action 和 mutation 已经在 store 中。但是 state 和 getters 还没有。这就是接下来的 resetStoreVM 方法做的事情。

resetStoreVM

resetStoreVM 函数中包括初始化 store._vm,观测 state 和 getters 的变化以及执行是否开启严格模式等。state 属性赋值给 vue 实例的 data 属性,因此数据是可响应的。这也就解答了问题 3,用到的属性在 state 中也必须要提前定义好,vue 视图才可以响应。

function resetStoreVM(store, state, hot) {  //保存老的vm  const oldVm = store._vm;  // 初始化 store 的 getters  store.getters = {};  // _wrappedGetters 是之前在 registerGetter 函数中赋值的  const wrappedGetters = store._wrappedGetters;  const computed = {};    forEachValue(wrappedGetters, (fn, key) => {    // 将getters放入计算属性中,需要将store传入    computed[key] = () => fn(store);    // 为了可以通过this.$store.getters.xxx访问getters    Object.defineProperty(store.getters, key, {      get: () => store._vm[key],      enumerable: true // for local getters    });  });  // use a Vue instance to store the state tree  // suppress warnings just in case the user has added  // some funky global mixins  // 用一个vue实例来存储store树,将getters作为计算属性传入,访问this.$store.getters.xxx实际上访问的是store._vm[xxx]  const silent = Vue.config.silent;  Vue.config.silent = true;  store._vm = new Vue({    data: {      $$state: state    },    computed  });  Vue.config.silent = silent;  // enable strict mode for new vm  // 如果是严格模式,则启用严格模式,深度 watch state 属性  if (store.strict) {    enableStrictMode(store);  }  // 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁  if (oldVm) {    if (hot) {      // dispatch changes in all subscribed watchers      // to force getter re-evaluation for hot reloading.      store._withCommit(() => {        oldVm._data.$$state = null;      });    }    Vue.nextTick(() => oldVm.$destroy());  }}复制代码

开启严格模式时,会深度监听 $$state 的变化,如果不是通过this._withCommit()方法触发的state修改,也就是store._committing如果是false,就会报错。

function enableStrictMode(store) {  store._vm.$watch(    function() {      return this._data.$$state;    },    () => {      if (process.env.NODE_ENV !== "production") {        assert(          store._committing,          `do not mutate vuex store state outside mutation handlers.`        );      }    },    { deep: true, sync: true }  );}复制代码

让我们来看一看执行完 resetStoreVM 后的 store 结构。现在的 store 中已经有了 getters 属性,并且 getters 和 state 都是响应式的。

 

 

 

至此 vuex 的核心代码初始化部分已经分析完毕。源码里还包括一些插件的注册及暴露出来的 API 像 mapState mapGetters mapActions mapMutation等函数就不在这里介绍了,感兴趣的可以自行去源码里看看,比较好理解。这里就不做过多介绍。

总结

vuex的源码相比于vue的源码来说还是很好理解的。分析源码之前建议大家再细读一遍官方文档,遇到不太理解的地方记下来,带着问题去读源码,有目的性的研究,可以加深记忆。阅读的过程中,可以先写一个小例子,引入 clone 下来的源码,一步一步分析执行过程。

作者:是呀呀呀
链接:https://juejin.im/post/5c6b7ccee51d454c643631f7
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的文章
JVM内存模型_Minor GC笔记
查看>>
SpringCloud学习之PassCloud——(一)PassCloud源代码下载
查看>>
Linux下安装Python环境并部署NLP项目
查看>>
Nginx篇-springCloud配置Gateway+Nginx进行反向代理和负载均衡
查看>>
Nginx篇-Nginx配置动静分离
查看>>
缓存篇-Redis缓存失效以及解决方案
查看>>
缓存篇-使用Redis进行分布式锁应用
查看>>
缓存篇-Redisson的使用
查看>>
phpquery抓取网站内容简单介绍
查看>>
找工作准备的方向(4月22日写的)
查看>>
关于fwrite写入文件后打开查看是乱码的问题
查看>>
用结构体指针前必须要用malloc,不然会出现段错误
查看>>
Linux系统中的美
查看>>
一些实战项目(linux应用层编程,多线程编程,网络编程)
查看>>
我觉得专注于去学东西就好了,与世无争。
查看>>
原来k8s docker是用go语言写的,和现在所讲的go是一个东西!
查看>>
STM32CubeMX 真的不要太好用
查看>>
STM32CubeMX介绍、下载与安装
查看>>
不要买铝合金机架的无人机,不耐摔,易变形弯曲。
查看>>
ACfly也是基于FreeRTOS的
查看>>