Vuex源码阅读(3):流程分析

这篇文章从入口文件开始解读 Vuex 源码,主要看我写在代码中的注释,我写的很详细。

由于源码内容比较多,一些不太重要的部分就不过多陈述了,想看更加具体的解析可以下载带注释的源码自行查看。

1,/src/index.js

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'

// 源码的主入口,抛出一系列的 API,包含 install 方法。
// 这个文件用于 es module 的打包

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

我们可以看到,index.js 使用 es6 的模块化规范导出了一个对象,这个对象就是我们在业务代码中使用 import Vuex from 'vuex' 语句导入的对象,由于 Vuex 是 Vue 的一个插件,所以我们先看 install 方法具体做了什么。

2,/src/store.js ==> install()

let Vue

export function install (_Vue) {
  // Vue 是当前模块的一个全局变量,该变量会在下面被赋值,这样做可以给当前作用域提供 Vue 对象。
  // 判断 Vue 变量是否已经被赋值,避免二次安装。
  if (Vue && _Vue === Vue) {
    // __DEV__ 出现在 rollup.config.js 中,replace 是 rollup 的一个插件,作用是:在构建代码的时候替换代码中的指定字符串
    // 这里就是做了一个判断,判断是不是开发环境。
    if (__DEV__) {
      // 如果是开发环境的话,发出警告,vuex已经安装,不用再次执行 Vue.use(Vuex) 了。
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 对 Vue 进行赋值
  Vue = _Vue
  // 执行 Vuex 的安装操作,安装的实现方法是利用Vue的mixin
  applyMixin(Vue)
}

install 函数还没有开始插件的安装工作,而是在安装之前进行一些判断防止二次安装,并将 _Vue 赋值给当前全局作用域中的 Vue 变量,具体介绍看上面的的注释,接下来看 applyMixin() 函数。

3,/src/mixin.js

export default function (Vue) {
  // 获取当前 Vue 的版本
  const version = Number(Vue.version.split('.')[0])

  // 这里会区分vue的版本,2.x和1.x的生命周期钩子是不一样的,如果是2.x使用beforeCreate,1.x即使用_init。
  if (version >= 2) {
    // Vue.mixin 的官方解释:全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
    // beforeCreate:生命周期钩子,在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
    // 所以,这一行代码的作用是:之后创建的每个 Vue 实例在 beforeCreate 阶段都会执行 vuexInit 方法。
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 重写 _init 方法,将 vuexInit 方法添加到每个Vue实例的 init 属性中。
    // 注意:_init 是 Vue 的生命周期方法;options.init 是用户在每个Vue实例中自定义的生命周期方法
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        // 如果用户自定义了 init 方法的话,就将 vuexInit 和 options.init 拼接进一个数组中
        ? [vuexInit].concat(options.init)
        // 否则的话,直接赋值 vuexInit
        : vuexInit
      // 利用 call 执行原始的 _init 函数。此时,每次 Vue 实例初始化的时候都会执行 vuexInit 函数
      _init.call(this, options)
    }
  }

  // Vuex 的初始化函数,作用是将 store 变量赋值给所有 Vue 实例的 $store 属性,这样我们就可以通过 this.$store 访问到 store 了。
  // 在这里,先看下我们在日常使用 Vuex 时的写法:

  // // src/store/index.js
  // import Vue from 'vue'
  // import Vuex from 'vuex'
  //
  // Vue.use(Vuex)
  //
  // const store = new Vuex.Store({
  //   ///
  // })
  //
  // export default store

  // // src/main.js
  // import Vue from 'vue'
  // import App from './App.vue'
  // import store from './store'
  //
  // Vue.config.productionTip = false
  //
  // new Vue({
  //   store,
  //   render: h => h(App)
  // }).$mount('#app')
  function vuexInit () {
    // 取出当前 Vue 实例的 $options,这个 $options 就是我们写的每个Vue实例的配置对象。
    const options = this.$options
    // 如果配置对象有 store 属性,说明当前的 Vue 实例是根节点,就像上面的 src/main.js 代码那样
    if (options.store) {
      // 给根节点的 $store 属性赋值
      this.$store = typeof options.store === 'function'
        // 如果 store 是函数的话,我们将其返回值赋值给 $store,
        // 这说明我们可以在 src/main.js 代码中,给 store 传递一个函数,不过这个函数的返回值必须是  Vuex.Store 的实例。
        ? options.store()
        // 如果 store 不是函数的话,直接将其赋值给 $store。
        : options.store
    // 如果当前节点有父节点,并且这个父节点有 $store 属性的话,将其赋值给当前节点的 $store 属性。
    // 就这样,一层一层的传递 store 变量,最终的效果就是所有的 Vue 实例都有 $store 这个属性。
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

看代码注释即可,我写的很详细。到目前为止,我们已经知道每个 Vue 实例是怎么获得 store 的了,接下来就看看这个 options.store 到底是什么。

4,上一小节的 options.store 到底是什么?

看下面的代码,这些代码都是我们业务层的代码:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

let store = new Vuex.Store({
  state: {},
  getters: {},
  actions: {},
  mutations: {},
})

export default store;
// app.js

import Vue from 'vue'
import Counter from './Counter.vue'
import store from './store'

new Vue({
  el: '#app',
  store,
  render: h => h(Counter)
})

options.store 其实就是我们 new Vue 时传递的配置对象中的 store 属性,而这个 store 属性是 Vuex.Store 类的实例。接下来看看这个 Vuex.Store 类的具体内容。

5,/src/store.js

读 Store 的源码主要是看其构造方法,主线思路都在这个构造方法中,看下面的源码及注释:

export class Store {
  constructor (options = {}) {
    // 如果尚未通过 Vue.use(Vuex) 安装 Vue,并且 window 全局变量有 Vue 属性的话,就为用户自动安装 Vuex。
    // 这种情况适用于通过 script 标签引入 Vue 和 Vuex 的情形。
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    // 如果当前是开发环境的话。
    if (__DEV__) {
      // assert 是 util.js 中的函数。
      // 如果第一个参数的 Boolean 为 false 的话,就抛出错误消息为第二个参数的 Error。
      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.`)
    }

    const {
      plugins = [],
      // boolean 值,如果为 true 的话,会使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
      strict = false
    } = options

    // 以下划线 _ 开头的变量是对象的内部变量,在这里初始化 store 的内部变量,这并不是 js 的语法,只是编码层次的约定。

    // 提交状态的标志,在_withCommit中,当使用mutation时,会先赋值为true,再执行mutation,修改state后再赋值为false,
    // 在这个过程中,会用watch监听state的变化时是否_committing为true,从而保证只能通过mutation来修改state
    this._committing = false
    // 用于保存所有action,里面会先包装一次
    // 在这里通过 Object.create(null) 创建空的对象,创建的对象的 __proto__ 指向 null,可以创建更加干净的空对象。
    this._actions = Object.create(null)
    // 用于保存订阅action的回调
    this._actionSubscribers = []
    // 用于保存所有的mutation,里面会先包装一次
    this._mutations = Object.create(null)
    // 用于保存包装后的getter
    this._wrappedGetters = Object.create(null)
    // 用于生成以及保存 module 树
    this._modules = new ModuleCollection(options)
    // 用于保存namespaced的模块,key 是 namespaced,value是对应的模块对象
    this._modulesNamespaceMap = Object.create(null)
    // 用于监听 mutation,这里对应官网的:https://vuex.vuejs.org/zh/api/#subscribe
    this._subscribers = []
    // 这个 _watcherVM (Vue实例) 用于实现:https://vuex.vuejs.org/zh/api/#watch
    this._watcherVM = new Vue()
    // 用于 getters 缓存
    this._makeLocalGettersCache = Object.create(null)

    // 使用 store 指向 this,作用和 const that = this; 是一样的。
    const store = this
    // 获取该类中定义的 dispatch 和 commit 方法。
    const { dispatch, commit } = this
    // 为 dispatch 和 commit 提供一层封装,使这两个函数中的 this 固定指向该类的实例,防止函数中的 this 指向被修改。
    // 如果你也想固定函数中的 this,可以借鉴这种思路:就是为目标函数提供一层封装函数,在封装函数中固定目标函数中 this 的指向,
    // 由于用户只能接触到这个包装函数,所以其无法更改目标函数中的 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)
    }

    // strict mode
    this.strict = strict
    // 获取根模块的 state
    const state = this._modules.root.state
    // 这个 state 是以模块的 state 为基准,相互嵌套的对象。例如:
    // {
    //   name: 'main module',
    //   foo: {
    //     name: 'foo module'
    //   },
    //   bar: {
    //     name: 'bar module',
    //     tar: {
    //       name: 'tar module'
    //     }
    //   }
    // }

    // 这里是module处理的核心,包括处理根module、action、mutation、getters和递归注册子module
    installModule(this, state, [], this._modules.root)

    // 使用vue实例来保存state和getter
    resetStoreVM(this, state)

    // 安装插件,安装的方法是执行插件函数,参数就是当前的 store 实例。
    plugins.forEach(plugin => plugin(this))
  }
}

这里,将只选几个重要的地方进行解读,其他部分自行查看源码注释。 

5-1,installModule(this, state, [], this._modules.root)

function installModule (store, rootState, path, module, hot) {
  // 判断是不是根模块
  const isRoot = !path.length
  // 获取指定模块的命名空间
  const namespace = store._modules.getNamespace(path)
  // console.log('[' + path + ']' + ":" + namespace)

  // 如果该模块的 namespaced 为 true 的话,说明该模块开启了一个命名空间
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    // 将命名空间以及模块设置进 _modulesNamespaceMap 中
    store._modulesNamespaceMap[namespace] = module
  }

  // 这段代码的作用是将 this._modules.root.state ({ name: "main module" })这个对象变成多级模块的 state 的集合体。
  if (!isRoot && !hot) {
    // 获取当前模块的父模块所对应的 state 对象。
    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('.')}"`
          )
        }
      }
      // 将该模块的 state 对象,设置到 parentState 中,并且 key 为当前的模块名称
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 设置当前模块的上下文。
  const local = module.context = makeLocalContext(store, namespace, path)

  // 逐一注册 mutation。
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 逐一注册action。
  module.forEachAction((action, key) => {
    // action 有可能是对象形式,看这里:https://vuex.vuejs.org/zh/guide/modules.html 中的 在带命名空间的模块注册全局 action 部分。
    // 如果 root 为 true 的话,type 直接用 key,也就是全局作用域下的 action。否则加上 namespace
    const type = action.root ? key : namespace + key
    // 在 action 是对象的情况下,处理函数是对象的 handler 属性,所以用下面进行兼容
    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
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

这一部分的代码主要是对模块进行安装,安装操作主要包括:

(1) 生成模块相互嵌套的 state 对象;

// 这段代码的作用是将 this._modules.root.state ({ name: "main module" })这个对象变成多级模块的 state 的集合体,就像下面这个样子:
  // {
  //   name: "main module",
  //   foo: {
  //     name: "foo module"
  //   },
  //   bar: {
  //     name: "bar module",
  //     tar: {
  //       name: "tar module"
  //     }
  //   }
  // }
  // 如果不是根模块且 hot 为 false 的话。
  if (!isRoot && !hot) {
    // 获取当前模块的父模块所对应的 state 对象。
    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('.')}"`
          )
        }
      }
      // 将该模块的 state 对象,设置到 parentState 中,并且 key 为当前的模块名称
      Vue.set(parentState, moduleName, module.state)
    })
  }

(2) 注册该模块的 mutation;

// 逐一注册 mutation。
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
function registerMutation (store, type, handler, local) {
  // 首先判断store._mutations是否存在指定的 type,如果不存在的话给空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 向 store._mutations[type] 数组中添加包装后的 mutation 函数
  entry.push(function wrappedMutationHandler (payload) {
    // 包一层,commit 函数调用执行 wrappedMutationHandler 时只需要传入payload
    // 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
    handler.call(store, local.state, payload)
  })
}

(3) 注册该模块的 Action;

// 逐一注册action。
module.forEachAction((action, key) => {
  // action 有可能是对象形式,看这里:https://vuex.vuejs.org/zh/guide/modules.html 中的 在带命名空间的模块注册全局 action 部分。
  // 如果 root 为 true 的话,type 直接用 key,也就是全局作用域下的 action。否则加上 namespace
  const type = action.root ? key : namespace + key
  // 在 action 是对象的情况下,处理函数是对象的 handler 属性,所以用下面进行兼容
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})
// registerAction(store, type, handler, local)
function registerAction (store, type, handler, local) {
  // 首先判断 store._actions 是否存在指定的 type,如果不存在的话给空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // 和 registerMutation 一样,向 store._actions 中 push 包装过的函数
  entry.push(function wrappedActionHandler (payload) {
    // 这里对应 Vuex 官网的:https://vuex.vuejs.org/zh/guide/modules.html 中的 "在带命名空间的模块内访问全局内容(Global Assets)"部分
    // action 函数的第一个参数是包含多个属性的对象,具体实现如下所示:
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,

      getters: local.getters,
      state: local.state,

      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // 判断 action 函数的返回值是不是 promise,如果不是的话,将其包装成 resolved 状态的 promise,确保其返回值是 promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }

    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

(4) 注册该模块的 Getter;

// 逐一注册getter
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
// registerGetter(store, namespacedType, getter, local)
function registerGetter (store, type, rawGetter, local) {
  // 由于 getter 是取值操作,所以不允许有两个相同的 getter
  if (store._wrappedGetters[type]) {
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }

  // 将 rawGetter 包装一层,并保存到 _wrappedGetters 对象中。
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

(5) 注册完上面这些后,要对这个模块的子模块进行注册,依次迭代注册所有模块。

// 逐一注册子module
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

注册完的状态如下所示:

5-2,resetStoreVM(this, state)

在这个函数中,将上面注册的 state 和 getters 存放到一个 Vue 实例中。

// resetStoreVM(this, state)
function resetStoreVM (store, state, hot) {
  // 保存旧的vm,这个 Vue 实例就是用来实现 Store 中数据到页面响应的关键之处。
  const oldVm = store._vm

  // 给 Store 实例设置 getters 对象
  store.getters = {}
  // 给 Store 实例设置 _makeLocalGettersCache 对象
  store._makeLocalGettersCache = Object.create(null)
  // 获取我们在 registerGetter 函数中设置的 _wrappedGetters,就像下面这个样子。
  // _wrappedGetters:
  //   evenOrOdd: ƒ wrappedGetter(store)
  //   fooGet: ƒ wrappedGetter(store)
  //   joo/jooGet: ƒ wrappedGetter(store)
  //   joo/op/c1Get: ƒ wrappedGetter(store)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 对 wrappedGetters 进行遍历
  forEachValue(wrappedGetters, (fn, key) => {
    // export function partial (fn, arg) {
    //   return function () {
    //     return fn(arg)
    //   }
    // }
    // 将 getter 函数添加到 computed 对象中
    computed[key] = partial(fn, store)
    // 这一段很有意思,我们给 store.getters 设置属性,key 是 getter 的路径加上 getter 名称,例如:joo/op/c1Get。
    // 然后,设置的 get 从 _vm 中取值。这使得我们可以通过 this.$store.getters.xxx 取得 getter 值,并且是响应式的。
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 对应的文档:https://cn.vuejs.org/v2/api/#silent
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      // 将 state 放到这里,使其具有响应式特征。在上面的 Store 类中有 state 的 get,具体如下所示:
      // get state () {
      //   return this._vm._data.$$state
      // }
      // 我们可以看到 Store 的 state 的 get 是从 _vm._data.$$state 中取值。
      // 这使得我们可以通过 this.$store.state.xxx 拿到我们在 Store 中定义的 state 值,并且是响应式的。
      $$state: state
    },
    // computed 用于构造 _vm,这使得从 computed 中拿到的值变成响应式的了。用于实现 this.$store.getters.xxx
    computed
  })
  Vue.config.silent = silent

  // 对应官方文档:https://vuex.vuejs.org/zh/api/#strict
  // 使 Vuex store 进入严格模式,在严格模式下,任何 mutation 处理函数以外修改 Vuex state 都会抛出错误。
  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())
  }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值