[vuex]源码学习笔记

314 阅读3分钟

这篇是对vuex源码的学习笔记,比较适合对vue有一定基础的同学。
通过对源码的阅读主要解决一下几个问题:

  • 1,在组件中我们是怎么使用this.$store获取到store
  • 2,vuex的响应式实现
  • 3,我们在module里面定义的actionmutationgetter在使用时不用带上module的name
  • 4,state必须通过mutation修改这个又是怎么实现的

基于上面的几个问题我看一下源码

一,入口

vuex作为一个vue插件,在分析时我们首先从install方法入手 install源码地址

// 对源码有做删减
export function install (_Vue) {
  Vue = _Vue
  applyMixin(Vue)
}

很简单做了两件事:

  • 给vuex的全局变量Vue赋值(这个很重要,不要忽略喔
  • 调用applyMixin

继续看applyMixin源码

// 对源码做了删减
export default function (Vue) {
    Vue.mixin({ beforeCreate: vuexInit })
  
  function vuexInit () {
    const options = this.$options
    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注册一个全局的(会应用到所用组件上)beforeCreate()
  • beforeCreate也就是组件被创建时,最初this.$store = options.store也就是我们传递进来,之后子组件从父级继承$store
new Vue({
    store
}).$mount('#app')

这样就解决了我们的第一个问题。

二,正式亮相

我们在使用vuex的时候一般会有两步:

    // 第一步
    Vue.use(Vuex)
    // 第二步
    var store = new Vuex.Store({...})

第一步也就是执行前面讲解的install(不懂的可以去了解下vue的插件机制),接下来我们主要了解下new Vuex.Store到底干了什么事 Vuex.Store源码

// 对源码做了删减
constructor (options = {}) {
    ...
    // 是否是通过commit mutation改变state标记
    this._committing = false
    // 第一件事
    this._modules = new ModuleCollection(options)
    // 第二件事
    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)
    }
    const state = this._modules.root.state
    // 第三件事
    installModule(this, state, [], this._modules.root)
    // 第四件事
    resetStoreVM(this, state)
    ...
}

接下来我们细说上面的4件事:

第一件事:

这块我不太想细说,这里我只告诉这件事产生了什么结果,大家有兴趣的可以自己去了解(这块东西很多,很重要,但不影响我们理解整个流)
主要是调用ModuleCollection()从名字就可以看出来什么功能了,模块采集(百度翻译,哈哈哈哈~)
我们在用vuex的时候module是这样写的

经过ModuleCollection处理后变成这样

其实我在这里换个写法大家就会很清楚了(希望不会被打)

// vuex不支持这种写法,只是给大家一个直观的感受
let root = {
  state: {},
  mutations: {},
  actions: {},
  _children: {
    people:{
      state: {},
      mutations: {},
      actions: {},
    },
    brid: {}
  }
}
let store = new Vuex.Store(root)
// 

this._modules.root和我们面传给Vuex.Store的结构类似。这样大家对第一件事产生的结果应该了解了吧

第二件事:

这块也没什么好说的,主要是给class Store下面的 dispatchcommit执行时绑定this为store

第三件事:

installModule源码

// 对源码有删减
function installModule (store, rootState, path, module, hot) {
    // 
  const local = module.context = makeLocalContext(store, namespace, path)
    // 注册mutation到store上
  module.forEachMutation((mutation, key) => {
    registerMutation(store, namespacedType, mutation, local)
  })
    // 注册action到store上
  module.forEachAction((action, key) => {
    registerAction(store, type, handler, local)
  })
    // 注册getter到store上
  module.forEachGetter((getter, key) => {
    registerGetter(store, namespacedType, getter, local)
  })
    // 递归调用处理更深层次的module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

两个点需要了解:

  • 1,主要过程是把module里面的action,mutatio,getter直接挂在store._mutations,store._actions,store._wrappedGetters上
// 定义下面的
let store = new Vuex.Store({
    modules: {
        moduleA: {
            state: {},
            actions: {
               actionA(){} 
            }
        }
    }
})
// 在组件中,我们不需要通过this.$store.dispatch('moduleA.actionA')来调用
this.$store.dispatch('actionA')
  • 2,local的值。

上面我们在调用this.$store.dispatch('actionA')时,传给actionA的state是对应moduleA下定义的state而不是整个state树

第四件事:

还是先看源码

// 对源码有删减
function resetStoreVM (store, state, hot) {

    // 拿到在第三步中挂在store上_wrappedGetters
  const wrappedGetters = store._wrappedGetters
  
  // 知识点一
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })
    // 知识点二
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 知识点三
  if (store.strict) {
    enableStrictMode(store)
  }

}

现在来细说上面三个知识点:

  • 知识点一:

    • 1,遍历所有的getter store._wrappedGetters,将getter挂在到一个computed对象上
    • 2,将所有的getter挂在到store.getters上(实现我们在代码中的getter调用this.$store.getters.xxxgetter)并重写对应getter的get方法(这个我们已经在vue的响应实现中见过了)
  • 知识点二:

    • new 一个Vue对象,将vuex中的state做?state的值传进去,因为JS中对象是引用类型的这里也就实现state的响应式了;
    • 同时将在知识点二中构造computed对象作为computed属性传进去,实现了getters的响应式和缓存。文章一开始的第2,3个问题被解决
  • 知识点三:

在开发环境一般开启严格模式,所以这里store.strict为true,enableStrictMode(store)会被调用。在相同文件里找到enableStrictMode的实现

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

这里主要就是监听this._data.?state也就是我们的state,如果被改变,在开发环境会通过store._committing来判断是否会抛出错误(store._committing为false则会抛出Do not mutate vuex store state outside mutation handlers的错误信息)。
class Store的构造函数中我们可以看到store._committing = false。那么store._committing会在哪里被改变呢,下一节进行说明

三,state必须通过mutation修改

mutation要通过commit提交,在同一个文件中找到commit的实现

 commit (_type, _payload, _options) {
    const entry = this._mutations[type]
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    this._subscribers.forEach(sub => sub(mutation, this.state))
  }
  • 1, 在store._mutations中找到我们要提交的mutation
  • 2,执行this._withCommit并将mutatio的执行作为它的回调

接着我们在同一个文件中找到**_withCommit**的实现

_withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
  • 1,将this._committing的默认状态(false)用committing保存起来
  • 2,将this._committing = true
  • 3,fn()执行,也就是执行我们commit中传给他的回调函数,其实就是调用mutatio改变state。

(注意,注意,注意)还记得我们在上节通过vue.$watch对state的进行监听吗,这里state改变了,在开发环境我们会通过store._committing判断是否是通过commit改变的来抛出错误。这里因为我们在2中将this._committing = true(通过mutation修改)所以不会抛出错误。
最后把this._committing = committing还原默认状态

**假设:我们我们通过其他任何方式修改了state,vue.$watch对state监听的回调被执行,因为store._committing默认为false,这时控制台会打印错误Do not mutate vuex store state outside mutation handlers

至此也就实现了state必须通过mutation修改

总结

之前在学习别人的源码分析文章时博主会对源码代码实现的细节做大量的讲解,反而让我越看越懵逼。
本文是带着问题来看的,首先告诉了你每一个知识点是干什么的、在哪里实现的,大家看了后会对vuex的源码有一个整体的认识。然后如果自己对某个知识点的实现比较感兴趣可以带着结果去理解过程,相信这样可以方便大家对源码的阅读。