这篇文章从入口文件开始解读 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())
}
}