vuex原理

每个组件(vue实例),在beforeCreate的生命周期中混入同一个Store实例作为属性$store。同时$store是响应式的。

Vuex 挂载

vue使用vuex就是通过Vue.use(Vuex),Vue.use本质上就是执行参数的install方法,如果参数本身就是函数,那么只执行这个函数。

install

let Vue;

export function install(_Vue) {
  // 避免重复加载
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        "[vuex] already installed. Vue.use(Vuex) should be called only once."
      );
    }
    return;
  }
  Vue = _Vue;
  applyMixin(Vue);
}

install函数会判断_Vue是否与传入的Vue参数相等,避免重复挂载。然后是执行applyMixin主逻辑

  • applyMixin

applyMixin函数通过Vue.mixinbeforeCreate的生命周期钩子执行 vuexInit方法。vuexInit方法则是将store注入每个vue实例。

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    // 正如前面所说的,store实例注入的生命周期是beforeCreate
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    // 兼容vue的1.x版本
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options // Vue传入的参数
    // store injection
    // 注入store
    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的实例对象,都回有$store属性,且是同一个Store实例。

Vuex.Store

传入的store实例是通过Vuex.Store类生成的,如官网示例代码:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

下面通过vuex.Store的源码逐步分析实例化store过程中发生了什么。

构造函数

export class Store {
  constructor (options = {}) {
    // 这个构造函数比较长,这里省略,后文分开细述
  }
}
if (!Vue && typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

如果是 cdn script 方式引入vuex插件,则自动安装vuex插件,不需要用Vue.use(Vuex)来安装。

// asset 函数实现
export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}
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.`)
}

实现asset函数的目的。尽早抛出异常,阻止程序运行,同时也方便后期拓展。

1.必须使用 Vue.use(Vuex) 创建 store 实例。
2.当前环境不支持Promise,报错:vuex 需要 Promise polyfill。
3.Store 函数必须使用 new 操作符调用。
const {
  // 插件默认是空数组
  plugins = [],
  // 严格模式默认是false
  strict = false
} = options

从用户定义的new Vuex.Store(options) 取出pluginsstrict参数。


// store internal state
this._committing = false
// 用来存放处理后的用户自定义的actoins
this._actions = Object.create(null)
// 用来存放 actions 订阅
this._actionSubscribers = []
// 用来存放处理后的用户自定义的mutations
this._mutations = Object.create(null)
// 用来存放处理后的用户自定义的 getters
this._wrappedGetters = Object.create(null)
// 模块收集器,构造模块树形结构
this._modules = new ModuleCollection(options)
// 用于存储模块命名空间的关系
this._modulesNamespaceMap = Object.create(null)
// 订阅
this._subscribers = []
// 用于使用 $watch 观测 getters,提供给外部使用
this._watcherVM = new Vue()
// 用来存放生成的本地 getters 的缓存
this._makeLocalGettersCache = Object.create(null)

声明Store实例对象一些内部变量。用于存放处理后用户自定义的actions、mutations、getters等变量。

// bind commit and dispatch to self
const store = this
const { dispatch, commit } = 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)
}
为什么要这样绑定?
确保调用commitdispatch函数的thissotre实例,比如下面的例子,当使用ES2015的参数解构时,如果没有绑定上下文,在严格模式下,thisundefined
actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
// 严格模式,默认是false
this.strict = strict
// 根模块的state
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

installModule函数,初始化根模块,并且递归遍历所有子模块,并且收集所有模块的gettter放置在_wrappedGetters中。

resetStoreVM函数, 初始化 store._vm 响应式,并且注册 _wrappedGetters 作为 它的computed的属性


// apply plugins
// 插件:把实例对象 store 传给插件函数,执行所有插件。
plugins.forEach(plugin => plugin(this))

// 初始化 vue-devtool 开发工具。这部分忽略
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)
}

整个构造函数的重点理解,其实也就是

this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)

installModule

function installModule (store, rootState, path, module, hot) {
  // 是根模块
  const isRoot = !path.length
  // 命名空间 字符串
  const namespace = store._modules.getNamespace(path)
  if (module.namespaced) {
    // 省略代码: 模块命名空间map对象中已经有了,开发环境报错提示重复
    // module 赋值给 _modulesNamespaceMap[namespace]
    store._modulesNamespaceMap[namespace] = module
  }
  // ... 后续代码 移出来 待读解释
}

设置 state

  // 不是根模块且不是热重载
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) {
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
      Vue.set(parentState, moduleName, module.state)
    })
  }
const local = module.context = makeLocalContext(store, namespace, path)
module.context 这个赋值主要是给 helpers 中 mapState、mapGetters、mapMutations、mapActions四个辅助函数使用的。
生成本地的dispatch、commit、getters和state。
主要作用就是抹平差异化,不需要用户再传模块参数

遍历注册 mutation

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

遍历注册 action

module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})

遍历注册 getter

module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

遍历注册 子模块

module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

resetStoreVM 函数 (重点)

resetStoreVM(this, state)

初始化store vm,它负责响应式,(也注册_wrappedGetters作为计算属性)
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store)
    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
  // store vm是就创建一个Vue实例,用来存储整个state树
  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
  if (store.strict) {
    enableStrictMode(store)
  }
  // 销毁
  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())
  }
}

store vm本质上就是一个Vue实例,它用来存储整个state树,利用vue的能力来实现响应式--$$state作为vue实例的data属性,而getter则作为vue实例的computer属性。

ModuleCollection

// new实例执行 this.register方法,注册根模块(Vuex.Store options)
var ModuleCollection = function ModuleCollection (rawRootModule) {
  // register root module (Vuex.Store options)
  this.register([], rawRootModule, false);
};

// 根据path路径获取对应的module对象,从根模块出发
ModuleCollection.prototype.get = function get (path) {
  return path.reduce(function (module, key) {
    return module.getChild(key)
  }, this.root)
};

ModuleCollection.prototype.getNamespace = function getNamespace (path) {
  var module = this.root;
  return path.reduce(function (namespace, key) {
    module = module.getChild(key);
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
};

ModuleCollection.prototype.update = function update$1 (rawRootModule) {
  update([], this.root, rawRootModule);
};

ModuleCollection.prototype.register = function register (path, rawModule, runtime) {
  var this$1 = this;
  // 若runtime不传,则默认为true,上文可知传的值为false
  if ( runtime === void 0 ) runtime = true;
  // 开发环境中,依次对传入的rawModule(Vue strore Option)的getter、mutation、action做类型校验(若存在)。
  if ((process.env.NODE_ENV !== 'production')) {
    assertRawModule(path, rawModule);
  }
  // 创建一个新模块,Module类, store'module的基础数据接口,本质上是树结构
  var newModule = new Module(rawModule, runtime);
  // path为[],则代表为根模块
  if (path.length === 0) {
    this.root = newModule;
  } else {
    // 获取带添加的的父模块节点,并添加新的模块
    var parent = this.get(path.slice(0, -1));
    parent.addChild(path[path.length - 1], newModule);
  }

  // register nested modules
  // 继续递归注册所有的modules对象
  if (rawModule.modules) {
    forEachValue(rawModule.modules, function (rawChildModule, key) {
      this$1.register(path.concat(key), rawChildModule, runtime);
    });
  }
};

ModuleCollection.prototype.unregister = function unregister (path) {
  var parent = this.get(path.slice(0, -1));
  var key = path[path.length - 1];
  var child = parent.getChild(key);

  if (!child) {
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(
        "[vuex] trying to unregister module '" + key + "', which is " +
        "not registered"
      );
    }
    return
  }

  if (!child.runtime) {
    return
  }

  parent.removeChild(key);
};

ModuleCollection.prototype.isRegistered = function isRegistered (path) {
  var parent = this.get(path.slice(0, -1));
  var key = path[path.length - 1];

  if (parent) {
    return parent.hasChild(key)
  }

  return false
};

参考文章

Vuex源码解析
了解vuex4.x基础用法与对比vue3.x的差异


看见了
876 声望16 粉丝

前端开发,略懂后台;