Vuex源码解析,拿来吧你!

217 阅读6分钟

前言

Vuex作为一款优秀的开源状态管理工具,官方的解释是“通过集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化”,所以我们只能通过提交mutation来修改状态的变化,同时为了分离同步与异步的操作,新增了dispatch来支持异步修改状态,也提供了状态的模块划分来支持状态的灵活配置,当然,还有其他的优秀概念,所以勾起我看其相关实现的好奇心。以及解决我看之前的疑问,本文使用的例子是Vuex库里提供的,examples/shpping-cart,建议大家先看看这个例子,并且把Vuex下载下来,方便大家理解跟一起复现😊。先抛出几个问题

  • vuex是怎么安装到vue的?
  • 为什么mutation是同步的,而dispatch可以执行异步?
  • 使用modules划分多个module后,子modules设置的mutations如何跟父module的区分使用?
  • Vuex是怎么变成响应式的

源码解析

安装

先看看我们是怎么使用vuex的:

// index.js
import Vue from 'vue'
import App from './components/App.vue'
import store from './store'
import { currency } from './currency'

Vue.filter('currency', currency)

new Vue({
  el: '#app',
  store, // 将store添加到根组件上
  render: h => h(App)
})
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
import cart from './modules/cart' // 子module
import products from './modules/products' // 子module

Vue.use(Vuex) // Vue.use(Vuex)安装Vuex插件

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({ // 将我们定义的状态信息通过Vuex.Store创建状态管理仓库
  modules: {
    cart,
    products
  }
})

可以看到,我们使用Vuex的步骤如下

  • 使用Vue.use(Vuex)安装Vuex插件
  • 将我们定义的状态信息通过Vuex.Store创建状态管理仓库
  • 将store添加到根组件上 ok,既然使用Vue.use,那么我们得提供install()方法供Vue执行安装逻辑,Vuex的入口文件是index.js,如下
// index.js
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'

export default {
  Store,
  install,
  ...
}

export {
  Store,
  install,
  ...
}

可以看到,我们抛出了install,这个方法是在store.js里定义

// store.js
export function install (_Vue) {
  ...
  Vue = _Vue
  applyMixin(Vue)
}

上面首先保存了我们传进来的Vue,这是一个在很多Vue插件里常用的操作,比方说Vue-router也是这样,这样可以直接在插件里使用Vue的功能,如状态响应更新,也不用额外在插件里安装Vue。接着就是执行applyMixin,看命名就是使用mixin做一些操作

// 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的指向,这样我们就可以通过this.$stor获取我们定义的state、getter等
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) { // 非根组件获取父组件的$store,最终都指向同一个$store
      this.$store = options.parent.$store
    }
  }
}

可以看到,是通过Vue.mixin设置beforeCreate这个生命周期,这样每个组件执行这个生命周期时就会调用vuexInit进行vuex的初始化,vuexInit作用就是设置每一个组件的this.$store指向我们在Vue根组件传入的store。所以到这里,每一个组件都可以通过this.$store获取我事先配置的在store.js里的state、getters等。

Store

我们在store.js调用Vuex.Store创建状态管理仓库,这个就是Vuex的核心,我们来看看他的构造函数

// store.js
export class Store {
  constructor (options = {}) {
    ...
    this._committing = false // 是否处于是通过_withComming提交修改state的
    this._actions = Object.create(null) // 保存action
    this._actionSubscribers = [] // 保存subscribeAction,会在action执行的前后调用
    this._mutations = Object.create(null) // 保存mutations
    this._wrappedGetters = Object.create(null) // 保存getters
    this._modules = new ModuleCollection(options) // 构造modules的集合
    this._modulesNamespaceMap = Object.create(null) // 保存命名空间
    this._subscribers = [] // 保存subscribeAction,会在mutations执行
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

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

    this.strict = strict

    const state = this._modules.root.state
    installModule(this, state, [], this._modules.root)

    resetStoreVM(this, state)

    ...
  }
...  

ModuleCollection

上面代码通过this._modules = new ModuleCollection(options)来构造modules的集合

// module/module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    this.register([], rawRootModule, false)
  }
  ...
  register (path, rawModule, runtime = true) {
    ...
    const newModule = new Module(rawModule, runtime) // 生成module
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule) // 添加当前modules到父moduels的_children数组里
    }

    // 如果当前modules下面还有modules则递归继续
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

可以看到,上面就是做了主要就是通过new Module来生成各个module,并将子module通过addChild添加到父module_children中。再看看Module

// module/module.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null) // 保存子modules
    this._rawModule = rawModule // 当前modules
    const rawState = rawModule.state // 当前modules的.state

    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }
...

本质上就是做格式化,将配置的modules转化成统一格式,然后通过_chidren关联起来,这样,后续我们就可以通过这个获取我们的所有状态信息,比如example/shopping-cart例子里,经过上面转化后变成如下:

B1876D79-126C-4509-B002-9B27F3B01ABE.jpg 可以看到每个节点都有相同属性,嵌套的modules变成对应的_children.

回到Store的构造函数,接下来就是执行

// store.js
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)
}

代码就是绑定dispatchcommit,先看下commit

commit

// store.js
commit (_type, _payload, _options) {
    const { 
      type, // 触发的mutations名
      payload, // 传入的数据
      options
    } = unifyObjectStyle(_type, _payload, _options)
    console.log(`type=${type}, payload=${payload}, options=${options}`)
    
    const mutation = { type, payload }
    const entry = this._mutations[type]
    ...
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload) // 执行mutations
      })
    })
    
    this._subscribers
      .slice()
      .forEach(sub => {
          console.log(`_subscribers after mutations`)
          sub(mutation, this.state)
      })

   ...
}

流程很简单,

  • 通过unifyObjectStyle获取触发的的mutations的名字type,跟传给mutation的数据payloadoptions是用于标识触发的mutations是全局modules里的mutations,还是当前modules里的mutations,是用于命名空间的,详细见这里
  • 然后就是找到对应的mutations然后执行,这里使用了this._withCommit包裹entry来执行mutations,这里先不管他的使用,后面会说
  • 接着就是调用this._subscribers来执行我们传给Vuex时使用store.subscribe定义的方法,store.subscribe是在mutations执行后才执行的,上面跑下example/shopping-cart例子看看那提交一个mutations各个位置输出什么,例子里这样设置subscribe
// logger.js
store.subscribe((mutation, state) => {
    const nextState = deepCopy(state)

    if (filter(mutation, prevState, nextState)) {
      ...
      logger.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
      logger.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
      logger.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))
      endMessage(logger)
    }
    ...
})

F3B63A10-4074-4302-824C-9DF65F06740A.jpg 可以看到,先执行了mutations,再执行subscribe,上面图里最后一行就log就是subscribe执行时打印的log

接下来看dispatch的代码

dispatch

// store.js
dispatch (_type, _payload) {
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)
    console.warn(`dispatch - type=${type}, payload=${payload}`)
    console.log(this._actionSubscribers, 'asdf=asd=fa=sdf=sd=f')
    const action = { type, payload }
    const entry = this._actions[type]
    ...
    try {  // dispatch执行前
      this._actionSubscribers
        .slice()
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
        ...
    }

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload))) // 执行dispatch
      : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(res => {
        try { // dispatch执行后
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
            ...
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          ...
        }
        reject(error)
      })
    })
}

可以看到,其实跟commit很类似,区别在于

  • _subscribers变成了_actionSubscribers,Vuex时使用store.subscribeAction定义的方法,且可以指定beforeafter,如果没指定before或者aftre,则默认是before
  • 使用promise来包裹执行函数,这也就是dispatch可以执行异步的原因。 同样的,我们打印一下触发dispatchlog,这里为了看到after,我稍微修改了一下例子,添加了一个after,执行结果如下

截屏2021-12-10 下午6.33.14.png 可以看到,先before,再执行dispatch,由于dispatch本质就是用异步的方式提交mutaions,所以这里会触发dispacth里的mutations操作,最后再执行after

接着再次回到Store,执行installModule进行模块的安装

installModule

// store.js
installModule(this, state, [], this._modules.root)

installModule主要分为三个部分:

  • 第一部分:设置module.state到父state上,这样就可以通过state.module.xxx获取子module的state,比如本文例子可以这样取state.cart.items,cart其实就是一个module
  • 第二部分:设置当前module的局部commit & dispatch,如果没有命名空间,则指向父commit & dispatch
  • 第三部分:注册模块的mustations、action、getters
// store.js
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length // 判断是否是根节点,因为只有根节点的为空
  const namespace = store._modules.getNamespace(path) // 获取命名空间, 会拼接父+子
  if (module.namespaced) { // 保存命名空间与当前module的映射
    ...
    store._modulesNamespaceMap[namespace] = module
  }
  // 第一部分
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父store的state
    const moduleName = path[path.length - 1] // 获取mudule的名字
    store._withCommit(() => { // 设置module到父state上
      ...
      console.log(`parentState = ${JSON.stringify(parentState)}, moduleName = ${moduleName}, module.state = ${JSON.stringify(module.state)}`)
      Vue.set(parentState, moduleName, module.state) // 设置module.state到父state上,这样就可以通过state.module.xxx获取子module的state, 疑问,这是使用Vue.set的目的是啥?直接这样不行?parentState[moduleName] = module.state
    })
  }
  
  // 第二部分,设置当前module的局部commit & dispatch,如果没有命名空间,则指向父commit & dispatch
  const local = module.context = makeLocalContext(store, namespace, path)
  
  // 第三部分
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

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

  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })
  
  // 递归子module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

第一部分

这一块首先获取获取父storestate,接着获取要设置到父state上的moudle,这样就可以就可以设置module.state到父state上,也就可以通过state.module.xxx获取子module的state,,比如本文例子可以这样取state.cart.items,我也截取了log日志 截屏2021-12-11 上午11.37.08.png 第一个log就是未设置的时候,第二个log就是设置第一个后的结果,可以看到,父state上已有cart这个moudle的state,这里我目前有个疑问🤔️,就是这行代码:

Vue.set(parentState, moduleName, module.state)

这里应该只是使用这个就行,有大佬知道的解答一下

parentState[moduleName] = module.state

第二部分

第二部分就是调用makeLocalContext设置moudle.context,有什么用呢?为的是在局部的模块内调默认可以用模块自己定义的actionmutation,是否局部取决于我们是否有设置命名空间。比如我们看example/shopping-cart里面cart这个module的定义(这个有设置命名空间namespaced: true,没截进来): 截屏2021-12-11 上午11.51.08.png addProductToCart是一个action,可以看到,如果未加{root: true},那么pushProductToCart触发的mutations是cart这个module自己的,如果加了,就会像红框一样,会去找父module的mutations,接下来看看makeLocalContext的定义

// store.js
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 // 拼接
        ...
      }

      return store.dispatch(type, payload)
    },

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

      store.commit(type, payload, options)
    }
  }

  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

首先看下是否有设置命名空间,没有则无需特殊处理,直接返回对应的dispatch/commit,否则需要重新拼接type值,因为我们如果在子module调用commit的话,如果没指定{root: true},那么其实是要调用子module自己的mutations,而不是父module的,所以得拼接拼接moduleName + type来找到需要触发的mutations。我们可以看下这里的namespace等于啥:

// module/module-collection.js
getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
}

可以看到,就是如果设置了module.namespaced = true那么就拼接当前模块的名字,即mouduleName,也就是里面的key,之所以使用path可以获取mouduleName,是因为path本质上就是父子module的mouduleName拼接而来的。

第三部分

第三部分用于注册模块的mustations、action、getters,这些都大同小异本质上只是做了挂载跟入参设置,比如把我们以其中的mutations为例,看看registerMutation(store, namespacedType, mutation, local)做了啥

// store.js
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) // 设置入参
  })
}

可以看到本质上就是把mutations挂在store._mutations,这样我们触发commit的时候,就可以通过store._mutations找到对应的mutations,然后设置入参数,这也就是我们可以在定义mutations函数时可以获取state,跟payload的原因,因为我们是这样定义mutations的(来源Vuex官网)

截屏2021-12-11 下午1.27.48.png

最后就是递归子module了,没啥可说🤷‍♂️。

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

ok,到这里installModules就结束了,再看看他做了啥?

  • 第一部分:设置module.state到父state上,这样就可以通过state.module.xxx获取子module的state,同时变成响应式,比如本文例子可以这样取state.cart.items
  • 第二部分:设置当前module的局部commit & dispatch,如果没有命名空间,则指向父commit & dispatch
  • 第三部分:注册模块的mustations、action、getters 通过上面的设置,我们得到了一颗完整的store树。如下:

截屏2021-12-11 下午1.34.38.png 这样我们就可以通过这个store树找到任何我们想要的state、mutations、action...

接下来再次回到Store的定义,只剩下最后一步了resetStoreVM(this, state)💥

resetStoreVM

这里做的就是初始化

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  store.getters = {}
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters // 获取installModules里挂在store._wrappedGetters里的getters
  const computed = {}
  // 循环getters,使用computed来指向我们的getters,并支持通过this.$store.getters.xxx能够访问到该getters
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({ // 这里类似bus总线
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent
  if (store.strict) {
    enableStrictMode(store)
  }
  ...
}

先想一下,我们是怎么使用getters的?,是通过this.$store.getters.xxx获取的,所以需要设置this.$store.getters.xxx指向对应的store._vm[xxx],这里的_vm就是下面的new Vue(),这里类似另一种通信方式,$bus总线,使用一个总线来保存所有的state、computed,这样订阅这些的属性想也就具有响应式了,但在编码中,我们是通过this.$sotre.state获取state的,所以还得做一层代理,如下:

// store.js
get state () {
    return this._vm._data.$$state
}

截屏2021-12-11 下午4.22.00.png 上图可以看到,_data下面就是保存了我们的$$state,也就是state,而computed通过 Object.defineProperty指向了._vm[key],也就是我们一个红框的部分。

_withCommit

ok,源码基本ok了,现在看下这个,源码里面多次出现_withCommit,他的作用是mutaitions的代理,所有涉及修改state的都得经过他才可以设置,看下他的代码

// store.js
_withCommit (fn) { // fn是mutations
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
}

可以看到,他在传入的fn前后设置committing,而这个committing是用来表示用户是通过mutations修改state,还是直接修改的,Vuex会watch state的变动,如果state被修改时this._committing为false,那么就会抛出警告,如下:

// store.js
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

我们通过_vm.$watch监听了 this._data.$$state的改动,如果改动时_committing = false,则代表我们是通过类似this.$store.state.xxx = xxx修改state的,而通过mutations的修改,由于使用了_withCommit包装了一层,那么就会在修改前把_committing改为true,就通过检测了,等mutations修改完state再重置为false。

结尾

至此,Vuex的核心流程就走完了,剩余如辅助函数、插件我觉得自行看源码即可。

现在回答开篇的三个问题:

  • vuex是怎么安装到vue的?

    通过install

  • 为什么mutation是同步的,而dispatch可以执行异步?

    dispatch使用promise包装了一层

  • 使用modules划分多个module后,子modules设置的mutations如何跟父module的区分使用?

    通过命名空间,设置了命名空间后,源码会根据是否设置了{root: true}来动态拼接实际的key,比如虽然example/shppping-cart的cart子module写了commit('pushProductToCart', { id: product.id }),但实际上触发的是cart这个module的pushProductToCart,而不是父module的pushProductToCart

  • Vuex是怎么变成响应式的?

    将所有的state,getters通过new Vue设置在的datacomputed里,然后将state,getters代理到new Vuedatacomputed里,这样读取后就自动响应式了.