vuex 源码阅读
记录第一亲自阅读源码
一、 源码目录结构分析
首先我们从 github 上把 vuex 的源码 clone 到本地, 项目目录文件较多, 我只标注出关键目录以及本次源码分析所用到的
├── examples // 官方提供的示例, 可用于调试代码
├── counter // 一个计数器相关的 vue 示例
│ ├── app.js
│ ├── Counter.vue
│ ├── index.html
│ └── store.js // store
│
├── index.html // 整体示例入口
├── server.js // 启动服务脚本
├── webpack.config.js // webpack 配置
├── src
├── module // 与模块相关的操作
│ ├── module-collection.js // 用于收集并注册根模块以及嵌套模块
│ └── module.js // 定义Module类,存储模块内的一些信息,例如: state...
│
├── plugins // 一些插件
│ ├── devtool.js // 开发调试插件
│ └── logger.js //
│
├── helpers.js // 辅助函数,例如:mapState、mapGetters、mapMutations...
├── index.cjs.js // commonjs 打包入口
├── index.js // 入口文件
├── index.mjs // es6 module 打包入口
├── mixin.js // 将vuex实例挂载到全局Vue的$store上
├── store.js // 核心文件,定义了Store类
└── util.js // 提供一些工具函数,例如: deepCopy、isPromise、isObject...
二、 调试环境配置
在终端中通过 cd
命令进入到 examples 目录里, 然后运行命令 node server.js
, 这样就会在本地启动一个服务, 我们在 Chrome 浏览器中去访问 http://localhost:8080/
, 点击 Counter, 看到的页面就是我们要调试的页面, 打开开发者工具。 然后去 src/store.js
打一些 debugger
, 然后刷新浏览器, 就会看到代码在 debugger
处暂停了, 说明我们的调试环境正常运行了。
这样, 我们就可以更加方便的去看代码的执行流程了
三、 源码阅读
1、 工具函数
我个人认为阅读源码首先要梳理整个库的工具函数, 因为这个库里调用最频繁的函数了, 所以我们首先看一下 src/util.js
文件中的工具函数
/**
* Get the first item that pass the test
* by second argument function
*
* @param {Array} list
* @param {Function} f
* @return {*}
*/
// list 是一个数组, 找到 list 中第一个符合 f 的项
export function find (list, f) {
return list.filter(f)[0]
}
/**
* Deep copy the given object considering circular structure.
* This function caches all nested objects and its copies.
* If it detects circular structure, use cached copy to avoid infinite loop.
*
* @param {*} obj
* @param {Array<Object>} cache
* @return {*}
*/
// 深度复制, catch 用于解决那些循环引用的对象 在深度复制时导致的死循环
export function deepCopy (obj, cache = []) {
// just return if obj is immutable value
if (obj === null || typeof obj !== 'object') {
return obj
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj)
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
/**
* forEach for object
*/
// 使用 fn 遍历 obj
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
// 判断 obj 是不是一个对象
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
// 判断 val 是不是一个 promise
export function isPromise (val) {
return val && typeof val.then === 'function'
}
// 断言
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
// 保留原始参数的闭包函数
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
以上就是所有的工具函数, 不多并且比较简单, 我都打上了注释, 稍微看一下就应该能看懂的
2、 入口文件
在 源码目录与分析 那里我们说明了入口文件为 src/index.js
, 我们拿 examples/counter
调试代码的时候, 入口文件也是指向的 src/index.js
, 这里可以查看 webpack.config.js
文件
resolve: {
alias: {
vuex: path.resolve(__dirname, '../src/index.js')
}
},
vuex 指向的就是 src/index.js
文件, 那我们就看看入口文件的代码
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
export {
Store,
install,
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
从入口文件可以看出, 这里主要导出了 Store 类、 install 方法以及一些辅助函数 (mapState、 mapMutations、mapGetters …)
我们在使用 vuex 的时候, 实例化 vuex 的时候,都是如下写法
const Store = new Vuex.Store({
// state,
// getters,
// actions,
// mutations,
})
所以 vuex 的核心代码在 store.js 中, 我们先从 store.js 看起
3、 Store 类的实现
store 的主要逻辑都是在 constructor
中完成的, 先看看 constructor
做了哪些工作
3.1 存放内部状态
constructor (options = {}) {
// 这个地方可以查看 github issues #731
// 如果 Vue 是 cdn 方式引入的, 那么会自动注册 Vuex
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// 对实例化 store 做一些校验
if (__DEV__) {
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 = [], // 插件
strict = false // 严格模式
} = options // 实例化 Store 对象的入参
// store internal state
this._committing = false // 表示提交的状态, 当通过 mutations 方法改变 state 时,该状态为true,state值改变完后,该状态变为false;
this._actions = Object.create(null) // 用于记录所有存在的actions方法名称(包括全局的和命名空间内的,且允许重复定义)
this._actionSubscribers = [] // 存放 actions 方法订阅的回调函数
this._mutations = Object.create(null) // 用于记录所有存在的的 mutations 方法名称(包括全局的和命名空间内的,且允许重复定义)
this._wrappedGetters = Object.create(null) // 收集所有模块包装后的的 getters(包括全局的和命名空间内的,但不允许重复定义)
this._modules = new ModuleCollection(options) // // 根据传入的 options 配置,注册各个模块,此时只是注册、建立好了各个模块的关系,已经定义了各个模块的 state 状态,但 getters、mutations 等方法暂未注册
this._modulesNamespaceMap = Object.create(null) // 存储定义了命名空间的模块
this._subscribers = [] // 存放 mutations 方法订阅的回调
this._watcherVM = new Vue() // 用于监听 state、getters
this._makeLocalGettersCache = Object.create(null) // getters 的本地缓存
// ... 其他剩余代码
}
关于各个变量的作用已写在了上边, 这里只有一句代码 this._modules = new ModuleCollection(options)
做了一些操作, 主要是是 递归注册各个模块,参数 options 就是我们定义的 store, 我们以 Counter 为例, 将其 store.js 文件中注册 store 的代码修改如下
const Store = new Vuex.Store({
modules: {
A: {
namespaced: true,
state,
getters,
actions,
mutations,
modules: {
B: {
namespaced: true,
state: {
count2: 2,
},
getters,
actions: {
cc: () => {}
},
mutations: {
dd: () => {}
}
}
}
}
}
})
对比之前, 主要就是区分了 module, 以及嵌套了一层 module
可以自行 console 一下, 看看 this.modules
的结构
3.2 递归注册 module
了解了 this.modules
的结构, 我们看看它具体是怎么实现的, new ModuleCollection(options)
位于 src/module-collection.js
中
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
// 注册根 module
this.register([], rawRootModule, false)
}
// ...
}
构造函数很简单, 就是调用 this.register
去注册 module
,
register (path, rawModule, runtime = true) {
// 如果是开发环境, 则校验 getters、 mutations、 actions 是否符合要求
if (__DEV__) {
assertRawModule(path, rawModule)
}
// 实例化一个 module, 第一次初始化(即执行构造函数的时候) runtime 为 false, path 为 []
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 然后将实例化好的 module 赋值给 this.root
this.root = newModule
} else {
// 嵌套 module 会执行到此处, 获取当前 key 的父对象
const parent = this.get(path.slice(0, -1))
// parent 为当前 newModule 的父对象, 该操作是在 parent._children 属性上绑定当前 newModule
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 如果 rowModule 中包含 modules, 那么循环注册 module
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
上述方法还用到一个值得介绍的函数 get
// 有嵌套 module 的时候会执行该, path 其实是嵌套 module 的名称,
// 该函数实现的是从 this.root 根 module 找到 path 对象的 module 对象
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
这里说一下 path
, path 代表各个模块的嵌套关系, 以之前配置的 Counter 为例
根 module 的 path 为 []
A module 的 path 为 [‘A’]
B module 的 path 为 [‘A’, ‘B’]
梳理一下 register 函数的逻辑
- 在开发环境下执行
assertRawModule
函数会校验 getters、 mutations、 actions 是否符合要求, getters 和 mutations 为必须为函数, actions 可以为函数, 也可以是对象, 但它的 handler 属性必须为函数 - 实例化一个 module 对象, 将 rawModule 和 runtime 作为参数, 初始化根 module 的时候,runtime 为 false, path 为 []
- path 的长度为 0, 说明是根 module, 将其赋值给
this.root
- 如果不为 0, 则说明有嵌套的 module,
path.slice(0, -1)
, 即省略最后一个, 那么此时执行 get 方法,则代表取当前 path 对应 module 的父 module, 在将当前 module 添加到父 module 的 _children 属性上 - 判断是不是子 modules, 如果有的话, 循环递归注册 module
注册 module 的逻辑按照上述其实已经介绍完了, 但是还有 new Module
没有介绍, 我们看一下 src/module/module.js
文件
import { forEachValue } from '../util'
// Base data struct for store's module, package with some attribute and method
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null) // 存储子 module
// Store the origin module object which passed by programmer
// 存储程序传递过来的原始 module 对象
this._rawModule = rawModule
// 原始对象里的 state
const rawState = rawModule.state
// Store the origin module's state
// 存储原始 module 里的 state, 如果 state 是函数, 则取返回结果, 否则取本身
// Tips: state 居然还可以是函数 厉害了 ୧(๑•̀◡•́๑)૭
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// 判断是不是设置了 namespace
get namespaced () {
return !!this._rawModule.namespaced
}
// 添加子 module
addChild (key, module) {
this._children[key] = module
}
// 删除子 module
removeChild (key) {
delete this._children[key]
}
// 获取子 module
getChild (key) {
return this._children[key]
}
// 判断不是不是拥有 key 的子 module
hasChild (key) {
return key in this._children
}
// 更新 module 的 actions、 mutations、 getters、 namespaced
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
// 遍历 this._children 执行 fn
forEachChild (fn) {
forEachValue(this._children, fn)
}
// 遍历 this._rawModule.getters 执行 fn
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
// 遍历 this._rawModule.actions 执行 fn
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
// 遍历 this._rawModule.mutations 执行 fn
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
这个类主要是设置了一个 module 的属性,以及操作 module 的方法, 这里发现了原来 state 可以写成函数形式,不看源码不知道哇
上述流程将 module 注册完成了, module 对象里初始化了 state, 但我们并没有看到 actions、 mutations、getters, 那么这三个属性是在哪注册的?
3.3 递归注册 module 的 getters、mutations、actions
我们继续回到 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)
}
// 严格模式下 修改 state 只能通过提交 mutation, 否则提示
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
// 初始化根 module 并且递归注册子 modules 和收集所有模块的 getters 放进 this._wrapperGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
// 初始化 store vm. 负责响应式
// 也将 _wrappedGetters 注册为 计算属性
resetStoreVM(this, state)
上述代码首先对 dispatch、 commit 进行了 this 绑定, 防止外面使用的时候对 this 进行重新绑定, 然后将根 module 下的 state 赋值给 state, 并执行 installModule 和 resetStoreVM 函数
3.3.1 installModule
function installModule (store, rootState, path, module, hot) {
// 是不是根 module
const isRoot = !path.length
// 获取 module 的 namespace, 格式 moduleA/moduleB/
const namespace = store._modules.getNamespace(path)
// register in namespace map
// 如果当前 module 的namespace true, 就以 namespace: module 的形式存储在 _modulesNamespaceMap
if (module.namespaced) {
// 如果 _modulesNamespaceMap 已经存在,且在开发环境, 控制台报错提示
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
// 不是根 module 且未开启 热重载
if (!isRoot && !hot) {
// 获取嵌套的 state, 获取当前 module 的父 module 的 state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取当前 module 的名字
const moduleName = path[path.length - 1]
// 提交结算, 执行回调函数期间, this._commiting 为 true
store._withCommit(() => {
if (__DEV__) {
// 如果当前 module 的 state 中有属性名和子 module 同名, 给出警告
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
// 在 parentState 上注册 moduleName, 并支持响应式
/**
* // Module A
* {
* count: 1,
* B: ModuleB.state
* }
*/
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
// 遍历当前 module 下的 Mutations, 并执行回调函数注册 mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 遍历当前 module 下的 Actions, 并执行回调函数注册 actions
module.forEachAction((action, key) => {
// action 可以为包含 handler 的对象, 也可以是 函数
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 遍历当前 module 下的 Getters, 并执行回调函数注册 getters
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 遍历当前 module 下的 children, 递归注册模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
以上就是 installModule 的全部代码
-
通过 path.length 是不是 0 判断是不是根 module
-
获取当前 module 的 namespace, 以之前 Counter 为例,B 的 namespace 为 A/B/, getNamespace 的逻辑自行去 module-collection.js 中查看
-
如果当前 module 有命名空间, 则将其存储在 _modulesNamespaceMap 中, 如果有同名的 namespace, 提示错误
-
初始调用, isRoot 为 ture, 我们先跳过
-
执行 makeLocalContext 并将结果赋值给 local 和当前 module 的 context 属性, 后续再看相应的逻辑, 这里只需知道他返回的是一个对象, 包含当前 module 的 dispatch、 commit、 getters、 state 四个属性, 其中 getters、state 不可枚举和修改
-
循环注册 mutation、 action、Getter
6.1 registerMutation
// 注册 mutation, 即存储在 store._mutations 数组里 function registerMutation (store, type, handler, local) { // 如果 store._mutations[type] 不存在, 则将其初始化为 [], 然后赋值给 entry const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { // handler 为 mutation 函数 handler.call(store, local.state, payload) }) }
注册 mutation, 即存储在 store._mutations 对象里,对象里的每一个 key 的值为数组, 也就是说, mutation 可以重复, 这里的 store 代表实例化的 Store 对象,还有一个注意点, 就是 type, 这个 type 是包含命名空间的, 还是以 Counter 中的 B module 为例, 这里的 type 为 A/B/dd, _mutations 数组里存储的是一个绑定好 this 的函数, handler 即为我们自定义的 mutations 函数
6.2 registerAction
// 注册 actions, 即存储在 store._actions 数组里, action 其实是一个 Promise function registerAction (store, type, handler, local) { // 如果 store._actions[type] 不存在, 则将其初始化为 [], 然后赋值给 entry const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload) { // action 函数 this 绑定为 store, 并传递如下参数 let res = handler.call(store, { dispatch: local.dispatch, // local 为处理好的对象,对有无 namespace 做了区分 commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload) // 如果 res 不是一个 Promise, 那么将其转化为 Promise if (!isPromise(res)) { res = Promise.resolve(res) } // 如果使用了 vue-devtools 调试工具, 那么则监听错误, 否则直接返回 if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) }
这里的逻辑同 registerMutation, 只不过是将绑定好 this 的 action 函数存储到 _actions 对象里,action 也可以重复, 还有我们编写的 action 函数返回结果被处理成了 promise, 所以可以这样
dispath(type, payload).then(() => {})
写6.3 registerGetter
// 注册 getter, 将 getter 存储在 store._wrappedGetters 对象中 function registerGetter (store, type, rawGetter, local) { // 如果存在, 则说明重复声明, 报错提示并返回 if (store._wrappedGetters[type]) { if (__DEV__) { console.error(`[vuex] duplicate getter key: ${type}`) } return } store._wrappedGetters[type] = function wrappedGetter (store) { // rawGetter 为 getter 的每一项 return rawGetter( local.state, // local state local.getters, // local getters store.state, // root state store.getters // root getters ) } }
getter 的 key 不允许重复, 所以开始做了一个判断, 然后将处理好的函数存储在 _wrappedGetters 对象中, rawGetter 为我们自己编写的 getter 函数
-
当前 module 注册完 getter、 mutation、action 以后, 看看有没有子 module, 然后遍历递归注册子 module 的 getter、 mutation、 action
-
递归注册的时候就会走到
if (!isRoot && !hot)
逻辑里, 这里主要是处理 state, 并将 state 处理成响应式的,还是以 Counter 为例, 此时根 state 结构如下state: { A: { B: { count2: 2 }, count: 1 } }
3.3.2 makeLocalContext
/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
* 如果没有 namespace, 就直接使用根 state 上的 dispatch、 commit、 getters 和 state,
* 否则 做一些小小的处理
*/
function makeLocalContext (store, namespace, path) {
// 判断有误 namespace
const noNamespace = namespace === ''
const local = {
// 如果没有 namespace, 就用 store.dispatch, 否则就用如下新函数
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, 例如 moduleA 下的 type 则为 A/type
type = namespace + type
// 如果为开发环境, 且当前 store._actions[type] 不存在, 报错提示,返回
if (__DEV__ && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
// 然后执行 store.dispath, 注意,如果 !option || !options.root, 此时的 type 包含包含模块名 A/type
// 如果 传递的 option.root, 此时同没有 namespace
return store.dispatch(type, payload)
},
// commit 逻辑同 dispatch
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
if (__DEV__ && !store._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
// getters 和 state 必须懒加载, 因为他们会在 vm 更新的时候发生变化
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
// 获取当前 path 指向的 state
get: () => getNestedState(store.state, path)
}
})
// 返回处理好的 local 对象
return local
}
该函数的主要作用就是将 dispatch、 commit、 getters、 state 本地化,即和 module 的命名空间相绑定, dispatch 和 commit 的处理逻辑一致, 如果有命名空间, 那么触发 dispatch 和 commit 的时候 type 会拼接上 module 名, 例如 A 下的 type 为 A/type。 如果没有命名空间, 那么直接调用根 module 的 dispatch 和 commit
因为 getters 和 state 会在 vm 更新的时候发生变化, 所以他们必须懒加载。 getters 的获取也区分命名空间, 如果没有命名空间, 则取全局的 getters, 即 store.getters, 它是在 resetStoreVM 方法中收集的, 如果有命名空间, 那么会取 makeLocalGetters 函数的返回值
function makeLocalGetters (store, namespace) {
// 如果 _makeLocalGettersCache 缓存里没有, 则执行以下逻辑
if (!store._makeLocalGettersCache[namespace]) {
const gettersProxy = {}
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
// type 格式 moduleName/getterName, type.slice(0, splitPos) 正好为 namespace
// 如果不相等,直接返回
if (type.slice(0, splitPos) !== namespace) return
// extract local getter type
// 获取 getter 的函数名
const localType = type.slice(splitPos)
// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
// 在 gettersProxy 上添加 当前 getterName 的属性, 值为 store.getters[type], 可枚举
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
// 保存在 _makeLocalGettersCache 对象上
store._makeLocalGettersCache[namespace] = gettersProxy
}
// 否则, 直接返回 _makeLocalGettersCache 缓存的内容
return store._makeLocalGettersCache[namespace]
}
这个函数对 getters 做了一层缓存,然后在有命名空间的情况下做了一层代理, 当我访问某个 getter 时,其实访问的是 moduleName/getter
获取 state 的话, 因为 state 已经处理过了, 处理成这个样子
state: {
A: {
B: {
count2: 2
},
count: 1
}
}
所以, 获取 state 就是通过 path 在上面的结构中一层一层的找,找到返回
// 获取 path 路径对应的 state, 上层的 module.state 会包含子 module 的 state
function getNestedState (state, path) {
return path.reduce((state, key) => state[key], state)
}
3.3.3 resetStoreVM
// 重置 vm
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
// 存储公共的 getters
store.getters = {}
// reset local getters cache
// 重置 getters 的缓存
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
// 将 getter 做成 Vue 里的计算属性, 实现缓存, store._vm[key] 其实取的就是 computed 绑定在 vue 实例上的 getter
computed[key] = partial(fn, store)
// 对 store.getters 做了一层代理, 当我想去取 store.getters[key] 的时候, 其实取的是 store._vm[key]
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
// 使用 Vue 实例去存储 state 状态树
// 关闭警告以防止用户添加了一些不好的全局 mixins
const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
// 如果使用了严格模式
if (store.strict) {
// 深度监测 状态树 的不合规变更, 并抛出错误
enableStrictMode(store)
}
// 如果存在老的 vm. 执行 destory 销毁掉
// 动态注册模块的时候会存在 oldVm, 需要销毁之前的 vm, 以免造成错乱
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())
}
}
该函数的主要作用就是初始化(重置) store, 以及处理 getter, 将 getter 放到 vue 的 computed 属性中去, 做成计算属性,已实现缓存等逻辑, 计算属性实际上也是将属性绑定在 vue 实例上, 所以我们去取 getter 的时候, 其实是获取 vue 实例的 getter 属性, 上面关键代码已经做了注释, 这里就不再赘述
至此, vuex 的各个属性的注册都已经完成了, 那么我们如何访问呢? 上面 resetStoreVM 将 store 和 getter 分别放在 vue 实例的 data 和 computed 属性里, 访问的时候其实访问的是 vue 实例上的 state 和 getter 属性, 那么 action 和 mutation 是如何访问的呢?
3.4 访问 mutation 和 action
我们都知道, 在代码里我们访问这两个属性的时候分别使用 commit 和 dispatch 方法, 那我们就看一下这两个方法是怎么实现的
我们之前在分析 Store 的 constructor 方法的时候, 遇见了下面的代码
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, 防止用户在使用的时候 this 绑定出错, 归根到底, 还是执行的 commit 和 dispatch 函数
3.4.1 commit
commit (_type, _payload, _options) {
// check object-style commit
// 检测参数是否正确
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 从之前存储 mutaitons 的对象里取出相应的 mutation
const entry = this._mutations[type]
// 如果不存在给出错误提示
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 订阅相关, 当 mutation 完成回调以后, 执行每一个订阅函数
// 订阅函数接口 mutation 和经过 mutation 后的状态作为参数
// 如果订阅者同步调用退订, 浅复制可以阻止 iterator 无效
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
// 开发环境下不支持 option.silent 属性了
if (
__DEV__ &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
commit 函数首先调用 unifyObjectStyle 函数检查以及校正参数
function unifyObjectStyle (type, payload, options) {
if (isObject(type) && type.type) {
options = payload
payload = type
type = type.type
}
if (__DEV__) {
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
}
因为我们 commit 支持两种调用方式
-
type 为字符串
commit('increment', payload)
-
type 为对象
commit({ type: 'increment', payload })
上面的方法就是兼容这两种传参方式的, 如果最后解析到的 type 不是字符串, 那么直接抛出一个提示错误
继续看 commit 代码, 然后从 this._mutations 中取出相应的 mutation, 然后在 this._withCommit
函数里调用 mutation 函数
this._withCommit
函数
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
就是在执行 mutation 前将 _committing
置为 true, 执行完以后置为 false, 严格模式下会深度校验,如果不是通过 mutation 改变的 state, 会有报错提示, 具体查看 enableStrictMode 函数
然后会执行一些订阅了 mutation 的函数, 我们后续再说
3.4.2 dispatch
dispatch (_type, _payload) {
// check object-style dispatch
// 检测参数是否正确
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 从之前存储的 actions 的对象里取出相应的 action
const entry = this._actions[type]
// 如果 action 不存在, 给出错误提示并返回
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 订阅相关的。
// 执行 action 分发之前的回调函数
try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
// 执行 action 并返回一个结果, 为 promise
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 订阅相关 TODO
return new Promise((resolve, reject) => {
result.then(res => {
try {
// 执行 action 分发之后的回调函数
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
// 执行分发 action 以后 action 报错的回调
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
}
开始和 commit 函数一直,也会校验参数,因为他也支持两种格式的传参, 然后从 this._actions
中取出之前存储的 action 函数,这里也会遇到一些订阅相关的逻辑, 它比 commit 的订阅逻辑稍复杂, 它支持 action 执行前, 执行后,以及执行出错的一些订阅逻辑, 订阅逻辑稍后讲
然后执行 action 函数, 最后返回 一个 promise
至此, state、getters、mutation、action 触发逻辑也已经讲完了, 还剩一些其他的 API, 我们按照官方文档一个一个的看
3.5 其他 API
3.5.1 replaceState 替换 store 的根状态
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
更新 state
3.5.2 watch
// 监听 getter 函数的返回值, 如果返回值发生变化, 那么执行回调函数 cb, 最后一个参数 options 可选,
// 表示 Vue 的 vm.$watch 方法的参数
watch (getter, cb, options) {
if (__DEV__) {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
}
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
该函数里面也是调用的 vue 里面的 watch API
3.5.3 subscribe 订阅 mutation
// 订阅 store 的 mutation
// fn 回调函数 options 配置项
subscribe (fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
调用的 genericSubscribe 函数, this._subscribers
是一个全局的数组, 在分析 Store 的 constructor 时已经说明, this._subscribers
用于存放 mutations 方法订阅的回调, 我们看一下 genericSubscribe 函数实现
function genericSubscribe (fn, subs, options) {
// 如果 subs 中不存在 fn, 那么存在 options.prepend 则放在 subs 的头部,否则放尾部
if (subs.indexOf(fn) < 0) {
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
// 返回一个函数, 为在 subs 中删除 fn, 调用即取消订阅
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
可以根据 options.prepend 来决定函数 fn 是放在 this._subscribers
的头部还是底部, 返回一个函数,调用的时候则剔除掉, 用于取消订阅
然后我们回到 commit 函数
// 订阅相关, 当 mutation 完成回调以后, 执行每一个订阅函数
// 订阅函数接口 mutation 和经过 mutation 后的状态作为参数
// 如果订阅者同步调用退订, 浅复制可以阻止 iterator 无效
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
当执行 commit 的时候, 会遍历 this._subscribers
然后执行, 也就是执行完 mutation 函数以后, 执行下面的订阅函数, 然后将当前执行的 mutation 函数和 this.state 作为函数的参数
3.5.4 subscribeAction 订阅 actions
// 订阅 store 的 action
subscribeAction (fn, options) {
const subs = typeof fn === 'function' ? { before: fn } : fn
// subs 为对象, 可以包含三个属性 before、after、error
// before 代表分发之前调用
// after 代表分发之后调用
// error 代表分发 action 的时捕获抛出的错误
return genericSubscribe(subs, this._actionSubscribers, options)
}
最后也是调用 genericSubscribe, 只不过是把 fn 存储在 _actionSubscribers
里面, 这里的 subs 会被封装成一个对象,如注释所述
我们再回到 dispatch
// 执行 action 分发之前的回调函数
try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
// 执行 action 并返回一个结果, 为 promise
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 订阅相关
return new Promise((resolve, reject) => {
result.then(res => {
try {
// 执行 action 分发之后的回调函数
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
// 执行分发 action 以后 action 报错的回调
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
这里分三部分, action 函数执行前, action 函数执行后, action 函数执行出错都会执行相应的回调
3.5.5 registerModule 模块动态注册
// 动态注册一个模块
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
// 模块 path 必须是数组且不能为 空
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
assert(path.length > 0, 'cannot register the root module by using registerModule.')
}
// 根据 path 注册 rawModule
this._modules.register(path, rawModule)
// 重复之前的步骤
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
// 重置 store 以更新 getters
resetStoreVM(this, this.state)
}
动态注册一个模块, 开始会检查参数是否正确, 然后调用 this._modules.register
方法, 注册模块, 下面继续执行 installModule 注册 getters、 mutations、 actions, 最后执行 resetStoreVM 重置 vue 实例以及更新 getters, 上述三个方法在之前已经介绍过了, 这里就不再重复了。
3.5.6 unregisterModule 动态卸载模块
// 卸载一个动态模块
unregisterModule (path) {
if (typeof path === 'string') path = [path]
// path 必须是数组
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
// 移除当前 module
this._modules.unregister(path)
this._withCommit(() => {
// 移除 state 里属性
const parentState = getNestedState(this.state, path.slice(0, -1))
Vue.delete(parentState, path[path.length - 1])
})
// 重置当前 Store
resetStore(this)
}
卸载一个动态注册的模块,首先调用 this._modules.unregister
去删除掉之前注册 module,然后再移除 state 里的值,最后执行 resetStore
unregister 卸载 module
unregister (path) {
// 找到当前 module 的父 module
const parent = this.get(path.slice(0, -1))
// 获取当前 module 的 moduleKey
const key = path[path.length - 1]
// 找到要卸载的 module
const child = parent.getChild(key)
// 如果不存在 给出提示
if (!child) {
if (__DEV__) {
console.warn(
`[vuex] trying to unregister module '${key}', which is ` +
`not registered`
)
}
return
}
// 静态 module 的 runtime 为 false, 也就是说静态 module 不能卸载
if (!child.runtime) {
return
}
// 从当前 module 的父 module 移除本身
parent.removeChild(key)
}
resetStore
// 卸载一个 module, 重置 _actions、 mutations、 _wrappedGetters、 _modulesNamespaceMap
// 重新注册所有 modules 以及重置 vm
function resetStore (store, hot) {
store._actions = Object.create(null)
store._mutations = Object.create(null)
store._wrappedGetters = Object.create(null)
store._modulesNamespaceMap = Object.create(null)
const state = store.state
// init all modules
installModule(store, state, [], store._modules.root, true)
// reset vm
resetStoreVM(store, state, hot)
}
为什么要重新注册, 因为之前动态注册模块的时候, action、mutation 函数已经被添加进相应的数组里了, 重新注册,重新执行之前的流程, 就会把之前添加进去的给删除掉, 保证移除干净
3.5.7 hasModule 模块是否已经被注册
// 检测模块的名字是否被注册
hasModule (path) {
if (typeof path === 'string') path = [path]
if (__DEV__) {
assert(Array.isArray(path), `module path must be a string or an Array.`)
}
return this._modules.isRegistered(path)
}
底部是调用 this._modules.isRegistered
来确定是否已经注册过
isRegistered (path) {
// 找到当前 path 下对应的父 module
const parent = this.get(path.slice(0, -1))
// 最后一个为当前 module 的 key
const key = path[path.length - 1]
// 判断父 module 是否拥有 key 的子 module
if (parent) {
return parent.hasChild(key)
}
// 否则直接返回 false
return false
}
3.5.8 hotUpdate 热替换新的 action 和 mutation
// 热替换新的 action 和 mutation
hotUpdate (newOptions) {
this._modules.update(newOptions)
resetStore(this, true)
}
调用 this._modules.update
更新 action 和 mutation, 沿着函数调用关系, 一步步
// module-collection.js
update (rawRootModule) {
update([], this.root, rawRootModule)
}
// module-collection.js
function update (path, targetModule, newModule) {
// 开发环境下检测 getters、 actions、 mutations 是否符合规范
if (__DEV__) {
assertRawModule(path, newModule)
}
// update target module
// 更新目标 module 的 actions、 mutations、 getters
targetModule.update(newModule)
// update nested modules
// 递归更新子 module
if (newModule.modules) {
for (const key in newModule.modules) {
if (!targetModule.getChild(key)) {
if (__DEV__) {
console.warn(
`[vuex] trying to add a new module '${key}' on hot reloading, ` +
'manual reload is needed'
)
}
return
}
update(
path.concat(key),
targetModule.getChild(key),
newModule.modules[key]
)
}
}
}
// module.js
// 更新 module 的 actions、 mutations、 getters
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
3.6 install
Store 类的逻辑我们讲完了, 我们再来看看 install 方法
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (__DEV__) {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
当我们调用 Vue.use(vuex)
时,调用这个方法,先判断 vuex
是否已被注册,若已被注册,则不执行任何操作 ; 若没有被注册,则调用 applyMixin
方法,现在移步到 ./mixin.js
文件:
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
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
}
}
}
applyMixin
方法先判断了 Vue
的版本号,主要做的是一个向下兼容 Vue 1.x
的版本,这里我对 Vue 1.x
的版本不太熟悉,所以就直接看 Vue 2.x
版本的处理方式吧
通过 Vue.minxin
方法做了一个全局的混入,在每个组件 beforeCreate
生命周期时会调用 vuexInit
方法,该方法处理得非常巧妙,首先获取当前组件的 $options
,判断当前组件的 $options
上是否有 sotre
,若有则将 store
赋值给当前组件,即 this.$store
,这个一般是判断根组件的,因为只有在初始化 Vue
实例的时候我们才手动传入了 store
; 若 $options
上没有 store
,则代表当前不是根组件,所以我们就去父组件上获取,并赋值给当前组件,即当前组件也可以通过 this.$store
访问到 store
实例了
4. 辅助函数相关
辅助函数都写在在 helper.js 中,在介绍各个辅助函数之前,我们先把符篆函数用到的基础函数介绍一下
4.1 基础函数
4.1.1 normalizeNamespace 函数
/**
* Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
* @param {Function} fn
* @return {Function}
* 返回一个期望参数是 namespace 和 map 的函数, 然后他会处理 namespace 和 map 并返回函数 fn
*/
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
这个函数主要是处理 namespace 的, 这也就是为什么我们使用 mapXXX 这些辅助函数可以这样用
mapState('A', {
count: state => state.count,
}])
// 或
mapState({
count: state => state.A.count,
})
4.1.2 normalizeMap 函数
/**
* Normalize the map
* normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
* normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
* @param {Array|Object} map
* @return {Object}
*/
// 序列化 map, 将其转化为 对象数组, 格式如上
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
看官方的注释, 就知道这个函数是干什么用的了, 将传进来的 map 转化成对象数组, 每一项为 { key: value}
格式
4.1.3 isValidMap 函数
/**
* Validate whether given map is valid or not
* @param {*} map
* @return {Boolean}
*/
// 判断 map 是不是一个数组或者对象
function isValidMap (map) {
return Array.isArray(map) || isObject(map)
}
4.1.4 getModuleByNamespace
/**
* Search a special module from store by namespace. if module not exist, print error message.
* @param {Object} store
* @param {String} helper
* @param {String} namespace
* @return {Object}
*/
// 通过 namespace 在 store 中查找相应的 module, 如果 module 不存在, 打印错误信息
function getModuleByNamespace (store, helper, namespace) {
const module = store._modulesNamespaceMap[namespace]
if (__DEV__ && !module) {
console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
}
return module
}
基础函数介绍完了, 开始进入主题, 看看这些辅助函数是怎么实现?
4.2 辅助函数
4.2.1 mapState
/**
* Reduce the code which written in Vue.js for getting the state.
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
* @param {Object}
*/
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
// 开发环境下, state 既不是对象也不是数组, 提示错误
if (__DEV__ && !isValidMap(states)) {
console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
}
const normalizeStates = normalizeMap(states);
normalizeStates.forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 从 context 中取出 state 和 getters, context 中的数据是经过命名空间区分的
state = module.context.state
getters = module.context.getters
}
// 返回结果, 如果 val 是一个 函数, 那么通过 call 调用, 参数为包含命名空间的 state、getters
// 否则, 直接返回 state 中相应的值
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
// 在开发者工具中编辑 vuex getter
res[key].vuex = true
})
return res
})
这里的参数 namespace 和 state 经过 normalizeNamespace 函数处理以后, namespce 为字符串或 undefined, states 为我们传递的 state 对象或数组
先声明一个 res 变量, 用来存储我们处理好的 state 值
然后通过 isValidMap 校验参数 states 是否是数组或对象,否则报错提示
然后调用 normalizeMap 处理 states,得到一个 { key: value }
的对象数组, 然后遍历这个对象数组, 将 key 作为 res 的 key, 函数 mappedState 为值
function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 从 context 中取出 state 和 getters, context 中的数据是经过命名空间区分的
state = module.context.state
getters = module.context.getters
}
// 返回结果, 如果 val 是一个 函数, 那么通过 call 调用, 参数为包含命名空间的 state、getters
// 否则, 直接返回 state 中相应的值
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
// 在开发者工具中编辑 vuex getter
res[key].vuex = true
}
该函数首先获取根 state 和 getters, 如果有 namespace, 那么会通过 getModuleByNamespace 来获取对应的 module, 取出 module 对应的 state 和 getters 并覆盖之前跟 state 和 getters,module.context
之前分析过,存储的是当前 module 下的actions, mutations, getters, state。
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
最后判断 val 是不是 function, 如果是, 那么取 function 的返回值, 否则直接取 state 的值
mapState 在 computed 中使用, 返回的 res, 必须符合 computed 的格式, 所以 res[key] 的值为一个函数
4.2.2 mapMutations
/**
* Reduce the code which written in Vue.js for committing the mutation
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept another params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
* @return {Object}
*/
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
if (__DEV__ && !isValidMap(mutations)) {
console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
}
const normalizeMutation = normalizeMap(mutations)
normalizeMutation.forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
// 如果 mapMutations 的第二个参数为一个对象, 且 val 为 function 的情况下, val 的第一个参数为 commit, 后面的为 payload 和 options
/**
* eg.
* mapMutations('A', {
* increment: function (commit, payload) {
* commit('increment', payload)
* }
* })
*/
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
基本逻辑同 mapState, 只不过最后返回不太一样
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
如果 val 是函数, 即如下格式使用
mapMutations('A', {
increment: function (commit, payload) {
commit('increment', payload)
}
})
那么第一个参数为 commit, 后面的参数为 payload
如果 val 是字符串
mapMutations('A', ['increment'])
那么会返回 commit 函数, 并将 increment 作为参数 type 传递进去
4.2.3 mapGetters
/**
* Reduce the code which written in Vue.js for getting the getters
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} getters
* @return {Object}
*/
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
if (__DEV__ && !isValidMap(getters)) {
console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
}
const normalizeGetter = normalizeMap(getters)
normalizeGetter.forEach(({ key, val }) => {
// The namespace has been mutated by normalizeNamespace
val = namespace + val
res[key] = function mappedGetter () {
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
if (__DEV__ && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
这个也基本同上, 只不过对 val 进行了一下处理 val = namespace + val
支持两种用法
mapGetters('A', ['evenOrOdd'])
或
...mapGetters({
evenOrOdd: 'A/evenOrOdd'
})
4.2.3 mapActions
/**
* Reduce the code which written in Vue.js for dispatch the action
* @param {String} [namespace] - Module's namespace
* @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.
* @return {Object}
*/
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
if (__DEV__ && !isValidMap(actions)) {
console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
逻辑同 mapMutations
4.2.4 createNamespacedHelpers
/**
* Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
* @param {String} namespace
* @return {Object}
*/
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})
这个函数的作用是提前绑定命名空间, 而不需要一个一个的去绑定
举个例子
methods: {
...mapMutations('A', {
increment: function (commit, payload) {
commit('increment', payload)
}
}),
...mapActions('A', [
'decrement',
'incrementIfOdd',
'incrementAsync'
])
}
想使用模块 A 下面的数据, 需要在使用的时候绑定命名空间 A
如果我们使用了 createNamespacedHelpers, 就不需要这么做了
const { mapMutations, mapActions } = createNamespacedHelpers('A')
// ...
methods: {
...mapMutations({
increment: function (commit, payload) {
commit('increment', payload)
}
}),
...mapActions([
'decrement',
'incrementIfOdd',
'incrementAsync'
])
}
四、 总结
源码总算分析完了, 看代码的时候一步一步的捋着代码看, 看不懂的地方打个 debugger 看看运行结果, 整体 vuex 源码不是很难理解, 而且只有 1000 行左右就实现了一个全局响应式变量存储库, 很厉害
在写本篇文章的时候, 其实不知道怎么下笔, 大白话大白话一坨一坨的写,不过整体下来收获还是挺大的。
记录于此, 纪念自己第一次亲自阅读源码
吐槽: 文章写的太长了, 用的 typora 都有点卡了。。。还有, 为什么没有一点点成就感呢???