速览vuex源码

177 阅读6分钟

Vuex 源码不过千行,主要使用了 Store 类、ModuleCollection 类、Module 类,结构清晰,下面简单说说 Vuex 中一些主要的源码实现。推荐打开 Vuex 源码一同观看。👀

vuex使用方法

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

按照使用方法,依次看看 vuex 做了什么事情。 👊

Vue.use(Vuex)

首先,装载Vuex插件,Vue.use 方法会调用传入 plugin 的 install 方法。在源码 src/index.js 中发现了 install 方法

import { Store, install } from './store'

install函数调用了核心方法 applyMixin(Vue)applyMixin位于 src/mixin.js 文件,

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

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    ...
  }
  
  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
    }
  }
}

其核心作用在于把我们传入的store对象,挂载到后代的每个 Vue 实例上,让我们可以通过this.$store访问store对象。👊

new Vuex.Store()

接下来就是核心部分,实例化 Store 。看 constructor 函数运行发生了什么。

    ...
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)
    ...

首先定义了一堆的属性,重点的一行是 this._modules = new ModuleCollection(options)

ModuleCollection 类就是一个module 管理中心,负责注册、删除、标识根 module 等简单功能, options 是实例化Store时传入的参数。module 的定义如下

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割

ModuleCollection类

ModuleCollection 类的构造函数如下

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (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) {
      this.root = newModule
    } else {
      // 在 子模块 注册的时候会走这里
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
  ...
}

register 方法中,path 参数是为了实现命名空间的功能,保存了module的结构化信息。暂且不看new Module的逻辑。 if (path.length === 0) 因为构造函数传入的path为[],所以这个判断条件为ture,标识了root module。 接着一个递归判断 if (rawModule.modules) 如果有 modules属性,就重复调用 register 方法,path 参数则加上了当前的 module 名。

如果我们传入的option 是这样

{
  modules: {
    a: {...}
  }
}

递归的时候 path 就成了 ['a'] 这样了,为什么要记录 path 呢。

默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。

一旦一个模块使用了命名空间功能,我们就必须要知道模块的注册路径,才方便在用户写下 this.$store.a.b.c.state时,去查找对应模块的属性 。

Module类

Module 类的构造函数十分简单。只是简单的储存了传入的 new Store 时的 option ,并且提供了子模块的管理、重写 actionsmutations 方法功能。

export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
  ...
}

ok 属性初始化完成。接着继续看 Store 的构造函数方法。

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

重载了一下 dispatch 方法 和 commit 方法,让它们的 this 变成强绑定,免得发生 this 丢失对的情况。

假如不显示绑定,在这种情况下,this 指向就变化了

a = store.commit
a('xx') // error , this指向全局了

在看核心代码 installModule 函数

   // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    installModule(this, state, [], this._modules.root)
    

installModule

  // register in namespace map
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      重复的模块名称报错
    }
    store._modulesNamespaceMap[namespace] = module
  }
  ...
  const local = module.context = makeLocalContext(store, namespace, path)
  
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  ...

makeLocalContext 函数主要为了抹平有命名空间的情况下访问 commitdispatchstore 的差异化,自动在调用时加上 namespace 的路径。代码如下👇

function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === ''

  const local = {
    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]) {
          未定义时报错
        }
      }
      return store.dispatch(type, payload)
    },
    commit: ...
  }
  
  // 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
}

接下来则是把定义的 mutationactiongetter 挂载到 store 实例对象上。以 mutation 举例

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

我们看得,存储 mutation 的数据类型是一个数组,也就是说可以定义多个同名 的 mutationVuex 都会记录下来,是否调用我们可以在 commit 方法看到。👣

resetStoreVM

以上 store 对象就基本处理完成了,剩下最后一部,把 state 变成响应式对象,使得我们在改变 state 的时候,触发对应的副作用,比如 getters 的值更新,比如触发 watch 的回调。核心代码如下:

  store._vm = new Vue({
    data: {
      ?state: state
    },
    computed
  })

Vuex 直接使用了 Vue 实例绑定。computed 对象就是我们定义的 getter 包装而来的。

以上就是 new Store(option) 中所执行的所有代码了。

最后再看看我们用的最多的 commit 方法。👀

commit

  commit (_type, _payload, _options) {
    ...
    const mutation = { type, payload }
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    this._subscribers
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))
  }

获取 type 然后在 _withCommit 里面执行匹配到的mutation。这里也印证了我们上文的猜想,可以注册多个同名mutation

_withCommit 的方法很简单。👇

 _withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

保存了 committing 之前状态,然后把 _committing 设置为 true, 执行完 commitfn 后,在还原,在严格模式下,state 的写操作会判定 _committing 的状态,确保只有 commit能修改 state

最后还调用了 this._subscribers, 看了文档发现对应的功能如下。

subscribe(handler: Function): Function

订阅 storemutationhandler 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数

另外,这段代码有点与众不同,this._subscribers.slice().forEach ,所有订阅函数经过了一次浅拷贝。防止在订阅回调中同步取消订阅,修改了this._subscribers长度,导致forEach次数变少,issue

在执行一些回调的时候一定要多考虑回调可能带来的副作用。😲

由于水平有限,以上解读不保证100%✅,尽量依照源码观看,如有错误欢迎指正。 通篇浏览,代码过多,以后还是写一些小点😵。阅读源码本来之目的是为了学习作者的代码设计、理解,比如 Vuex 对于命名空间、module 的初始化、分割管理操作等,类的设计抽象等等。