Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于vuex学习&码源 #13

Open
Hibop opened this issue Dec 12, 2018 · 0 comments
Open

关于vuex学习&码源 #13

Hibop opened this issue Dec 12, 2018 · 0 comments

Comments

@Hibop
Copy link
Contributor

Hibop commented Dec 12, 2018

image

一、在Vue中使用Vuex的核心步骤:

  • 在Vue中安装 Vuex插件: Vue.use(Vuex)
  • new Store()实例,并插入到Vue的option中:new Vue({ store })
  • 使用this.$store.statethis.$store.getter得到state,this.$store.commit(mutation)this.$store.dispatch(action)[异步] 更新action;

二 、Vuex 源码的核心逻辑

  1. 安装插件
  2. store初始化
  3. 跟踪状态变化

插件安装原理

Vue.use(Vuex) 作为研究源码的入口,完成的第一个工作,就是在 Vue 中进行 Vuex 的安装工作。

关于 Vue.use() 的用法,官方文档的解释如下:

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法将被作为 Vue 的参数调用。

可以看出,插件安装的核心原理,就是调用插件所提供的 install 方法。它的源码 主要完成了以下几个工作:

  1. 判断插件是否已经被安装
  2. 初始化插件安装所需要的参数,包括安装对象:Vue 实例
  3. 调用插件的 install 方法,进行安装操作
// location: src/store.js
// line: 450
export 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
  applyMixin(Vue)
}


// applyMixin所在文件夹src/mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    const usesInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(usesInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // ...
  }

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

  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
    }
  }
}

Vuex 插件安装的本质,是调用了 Vue.mixin() 这个方法。这是Vue 为插件作者所提供的 混合机制 ,它在全局注册了一个混合,进而会影响到注册之后所有创建的每个 Vue 实例。
store 作为 Vue 实例的一个 option 被注入到根部组件,而后所有的子组件都从其父组件的 options 中去寻找这个 store 属性,这样层层传递下去,所有的组件就都可以通过 this.$store 来访问全局的 store 了。

store初始化

  • 在 store.js 的构造函数中,函数的一开始做了一系列的安全验证工作,整个 Vuex 中这样的验证工作做了很多,这对组件开发提供了一个很规范的思路,用最简洁的代码完成组件的安全验证,值得学习。
  • 完验证工作之后,构造函数对 state 、plugins、strict 这几个参数做了初始化工作
const {
  plugins = [],
  strict = false
} = options

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state()
}
// 解构赋值 的方式,将传入构造函数的参数进行内部定义,可以看到这里 state 也可以通过 function 的方式来定义。
  • 定义完 state 等一些参数之后,构造函数定义了一系列的内部状态
// location: src/store.js
// line: 28

// store internal state

// 表示提交状态
// 保证对 Vuex 中 state 的修改只能在 mutation 中进行,而不能在外部随意修改state。
this._committing = false
// 存放用户定义的所有的 actions
this._actions = Object.create(null)
// 存放用户定义的所有的 mutations
this._mutations = Object.create(null)
// 存放用户定义的所有的 getters
this._wrappedGetters = Object.create(null)
// 存放用户定义的所有的 modules
this._modules = new ModuleCollection(options)
// 存放 modules 和其 namespace 的对应关系
this._modulesNamespaceMap = Object.create(null)
// 用于 vuex 的相关插件,存放所有对 mutations 变化的订阅者
this._subscribers = []
// 用于 vuex 的相关插件,用来观测 state 的变化
this._watcherVM = new Vue()
  • modules 层次构建

在对上述的内部状态进行赋值的时候,Vuex 是按照 modules 的层次逻辑逐层展开的。最开始的 demo 中可以看出,Vuex 提供 modules 机制。在没有 modules 的情况下, 所有的 state, mutations, actions, getters 都会”挂载“在同一个节点上,随着系统规模的扩大,整个逻辑会显得非常臃肿,不便于维护。

modules 机制很好地解决了这个问题,不同模块将拥有自己独立的 state, mutations, actions, getters ,模块间公共的部分可以提炼出来挂在“根节点”上,私有的部分作为“子节点”,按照 命名空间 的指定规则依次进行挂载。

需要注意的是,Vuex 内部并不是一个树的结构,树的查找和插入操作复杂度都比较高,为了便于理解,这里用 挂载这个词来形容整个 modules 的层次构建。实际上 Vuex 是 “扁平化” 处理的,所有的 modules 都有一个 path 字段,来定义 modules 的层次结构,所以也可以理解为是一个 flatten 化的 tree

installModule 方法会根据 namespace 参数,递归 地对 modules 进行构建。

代码的前半段,取出了 module 的私有 state ,以 Vue.set() 的方法,挂载到 parentState 下面。

代码的后半段,对 module 内的 mutations,actions,getters进行注册操作。注意这里的 local 变量,它定义了每个 module 各自独立的 dispatch 和 commit 方法,实质就是,在设置了 namespace 的情况下,这两个方法会在 action / mutation 的 type 前面,加上 module 的 path,来完成正确的调用操作。

注册部分,以 mutations 为例,所有 modules 的 mutations 最终都存储在 store._mutaions 这个在之前预先定义好的私有变量中。

没有 namespace 的情况下,发起 this.$store.commit('increment', payload)操作之后,来自不同 module 的同名 mutations,最终会被同时调用。加上 namespace 之后,就可以进行独立调用:this.$store.commit('A/increment', payload),this.$store.commit('B/increment', payload)。

以如果模块之间涉及 同名 mutations 时,一定要在 module 的定义位置,加上 namespaces: true。

到这里位置,整个 store 的形状基本已经搭建完成了,state,actions,mutations,getters都已按照 modules 的层级关系,递归地完成了初始化。可以理解为:store 内部的 状态,以及对这些状态进行变更的 逻辑 ,都已填充完毕,接下来要做的就是绑定 commit 和 dispatch 这两个核心方法,来完成对逻辑的 调度 。

  • commit / dispatch 方法绑定, commit 和 dispatch 的逻辑较为相似

commit 方法的第一步,调用了unifyObjectStyle() 这个函数,由于 Vuex 的 commit 操作提供两种传参风格,所以要对传入的参数做统一处理。
然后 commit 方法会根据传入的 type 参数,去找到相应 mutation 的回调函数来执行,可以看到执行动作用 this._withCommit() 方法进行了包裹,这里就用到了前面提到的 this._committing 变量。

这个方法保证了所有的 state 变更操作,都会把 this._committing 置为 true ,一旦 state 变更时这个值为 false 时,就说明采用了异常方法进行了状态修改,然后就会进行报错。这个在下文的 严格模式 中会提及。

this._withCommit() 方法的内部,可以看到用了 entry.forEach() ,这就解释了前文提到的,在没有 namespace 的情况下,同名函数都会被执行的原因。

在完成 mutation 的回调之后,会调用 this.subscribers 中注册的所有回调,那些 Vuex 相关插件的订阅回调就会被执行。

到这里位置,整个 store 的初始化工作就算完成了,实现了 store 内部有关 状态(state),逻辑(mutations/actions/getters) 和 调度(commit/dispatch) 的填充。但整个 store 的构造函数并没有结束,在构造函数的最后,store 回答了那个困扰我很久的问题:为什么 getters 能够响应 state 的变化?

追踪状态变更

// location: src/store.js
// line: 56

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// apply plugins
plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

最后一行很容易理解,就是完成 Vuex 的插件注册,其中 devtoolPlugin 是内置的默认插件, Chrome 的 Vue 扩展中,里面的 Vuex 时光穿梭 功能,就是通过这个插件实现的。

  • state / getters 响应式原理: 重点在resetStoreVM(this, state)部分

这段代码的关键核心,就是 store._vm 这个变量,它的 本质是一个 Vue 实例 。在这个 Vue 实例中,state 作为 data 传入,而 computed 属性则挂载了所有的 getters!因此,通过 Vue 的响应式原理,Vuex 实现了 state 和 getters 的响应式跟踪。

到这里就可以解释清楚了,之所以 mutation 修改了 state 之后,组件会感知到 state 的变化,getters 也会感知到 state 的变化,是因为 Vuex store 的本质,是构建了一个 Vue 实例,而所有 mutations,actions 所涉及的逻辑操作,都是对这个 Vue 实例进行 data 修改。进而其实可以发现,Vuex 响应式的原理,和 Vue 官方提出的 中央事件总线 其实是一个道理的。

resetStoreVM(this, state) 的最后,对严格模式和热重载进行了响应的处理。

在 开发环境 下开启严格模式之后,Vuex 会跟踪所有 state 的变化过程,并且这是一种 深度跟踪 ,对复杂对象依旧有效。这么做的目的是,devtoolPlugin 能够记录 state 的状态和变化,并且所有不是通过 mutations 而触发的 state 变更,都会报错。在 生产环境 下,严格模式通常会被关闭,以此来提高性能。

另外,Vuex 提供 mutations / actions / modules 的 热重载 功能,上面代码片段的末尾部分,就是在 热重载 的模式下,强制刷新所有监听者(watchers),并且将上一个状态的 oldVm 销毁,节省内存。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant