Vuex 源码分析
dwqs opened this issue · 4 comments
之前大致分析过了 vue-cli 源码 和 vue-router 源码, 这两个工具也是 vue 生态中比较重要的组件. 而最近因为业务上的需要, 接触到了 vuex 的一些组件源码. vuex
集中于MVC模式中的Model层, 规定所有的数据操作必须通过 action
- mutation
- state change
的流程来进行, 再结合 vue
的数据视图双向绑定特性来实现页面的展示更新:
现在就具体来分析一下其流程.
写本文时, vuex 的版本是 2.4.0
目录结构
打开 Vuex
项目, 先了解下其目录结构:
Vuex提供了非常强大的状态管理功能, 源码代码量却不多, 目录结构划分也很清晰. 先大体介绍下各个目录文件的功能:
- module: 提供module对象与module对象树的创建功能
- plugins: 提供开发的辅助插件
- helpers.js: 提供
mapGetters
,mapActions
等 API - index.js/index.esm.js: 源码主入口文件
- mixin.js: 在 Vue 实例上注入
store
- util.js: 提供
vuex
开发的一系列工具方法, 如forEachValue
/assert
等
入口
在 2.4.0 中, vuex
提供了 UMD 和 ESM(ES module) 两个构建入口, 分别对应 src/index.js
和 src/index.esm.js
文件. 在入口文件中, 主要是导出 vuex
提供给 Vue 应用的 API:
// src/index.js
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
在 Vue 应用中, vuex
是作为 vue
的一个插件为 Vue 应用提供强大的状态管理功能. 因而, 在能够使用 vuex
的功能之前, 先要在 Vue 应用的入口文件中安装 vuex
:
// ...
import Vue from 'vue'
import Vuex from 'vuex'
// install vuex plugin
Vue.use(Vuex)
// ...
Vue 在安装插件时, 会去调用插件提供的 install
方法:
// src/store.js
import applyMixin from './mixin'
// ...
let Vue;
export class Store {
// ...
constructor (options = {}) {
// ...
// 浏览器环境下的自动安装:to fix #731
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// ...
}
}
// ...
export function install (_Vue) {
if (Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
// Vue 变量赋值
Vue = _Vue
applyMixin(Vue)
}
在 src/store.js
文件中, 先声明了一个局部变量 Vue
来保存Vue 引用, 该变量有如下作用:
- 插件不必将
Vue.js
作为一个依赖打包 - 作为避免重复安装的
vuex
的条件判断 - 在
Store
中调用 vue 全局 API 的提供者 - 创建 Vue 实例
在 install
方法中, 调用了 applyMixin
方法:
// src/mixins.js
export default function (Vue) {
// 获取当前 Vue 的版本
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 2.x 通过 hook 的方式注入
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 兼容 1.x
// 使用自定义的 _init 方法并替换 Vue 对象原型的_init方法,实现注入
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 注入
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 子组件从其父组件引用 $store 属性
this.$store = options.parent.$store
}
}
}
applyMixin
方法的主要功能将初始化 Vue 实例时传入的 store
设置到 this
对象的 $store
属性上, 子组件则从其父组件引用$store
属性, 层层嵌套进行设置. 这样, 任何一个组件都能通过 this.$store
的方式访问 store
对象了.
store对象构造
store
对象构造的源码定义在 src/store.js
中, 梳理源码之前, 先大致了解下其构造流程:
环境判断
在 store
的构造函数中, vuex
先对构造 store
需要的一些环境变量进行断言:
import { forEachValue, isObject, isPromise, assert } from './util'
//...
let Vue;
// ...
if (process.env.NODE_ENV !== 'production') {
// 根据变量 Vue 的值判断是否已经安装过 vuex
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
// 当前环境是否支持 Promise
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
// 是否是通过 new 操作符来创建 store 对象的
assert(this instanceof Store, `Store must be called with the new operator.`)
}
// ...
assert
函数的定义是在 src/util
中:
export function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`)
}
初始化变量
在环境变量判断之后, 在构造函数中会定义一些变量, 这些变量一部分来自 options
, 一部分是内部定义:
import ModuleCollection from './module/module-collection'
//...
let Vue;
// ...
// 从 options 中获取 plugins/strict/state 等变量
const {
plugins = [],
strict = false
} = options
let {
state = {}
} = options
if (typeof state === 'function') {
state = state()
}
/**
* store 内部变量
*/
// 是否在进行提交状态标识
this._committing = false
// 用户定义的 actions
this._actions = Object.create(null)
// 用户定义的 mutations
this._mutations = Object.create(null)
// 用户定义的 getters
this._wrappedGetters = Object.create(null)
// 收集用户定义的 modules
this._modules = new ModuleCollection(options)
// 模块命名空间map
this._modulesNamespaceMap = Object.create(null)
// 存储所有对 mutation 变化的订阅者
this._subscribers = []
// 创建一个 Vue 实例, 利用 $watch 监测 store 数据的变化
this._watcherVM = new Vue()
// ...
收集 modules 时, 传入调用 Store 构造函数传入的 options
对象, ModuleCollection
类的定义在 src/modules/module-collection.js
中:
import Module from './module'
import { assert, forEachValue } from '../util'
export default class ModuleCollection {
constructor (rawRootModule) {
// 注册根module
this.register([], rawRootModule, false)
}
// ...
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
// 对 module 进行断言, 判断 module 是否符合要求
// module 的 getters/actions/ mutations 等字段是可遍历的对象
// 且 key 的值类型是函数
assertRawModule(path, rawModule)
}
// 创建 module 对象
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 递归创建子 module 对象
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// ...
}
ModuleCollection
主要将传入的 options
对象整个构造为一个 module
对象, 并循环调用 register
为其中的 modules
属性进行模块注册, 使其都成为 module
对象, 最后 options
对象被构造成一个完整的组件树. 详细源码可以查看 module-collection.js.
Module
类的定义在 src/modules/module.js
:
import { forEachValue } from '../util'
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
// 当前 module
this._rawModule = rawModule
// 当前 module 的 state
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// ...
// 执行 installModule 时会用到的一些实例方法
forEachChild (fn) {
forEachValue(this._children, fn)
}
forEachGetter (fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachAction (fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
}
详细源码可以查看 module.js.
接着往下看:
// 绑定 this 到 store
const store = this
const { dispatch, commit } = this
// 确保 dispatch/commit 方法中的 this 对象正确指向 store
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)
}
// ...
上述代码主要是把 Store
类的 dispatch
和 commit
的方法的 this
指针指向当前 store
的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store
直接调用 dispatch/commit
方法时, 能够使 dispatch/commit
方法中的 this
指向当前的 store
对象而不是当前组件的 this
.
dispatch
的功能是触发并传递一些参数(payload)给与 type
对应的 action
, 其具体实现如下:
// ...
dispatch (_type, _payload) {
// 获取 type 和 payload 参数
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
// 根据 type 获取所有对应的处理过的 action 函数集合
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 执行 action 函数
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
//...
而 commit
会将 action type
提交给对应的 mutation
, 然后执行对应 mutation
函数修改 module
的状态, 其实现如下:
// ...
commit (_type, _payload, _options) {
// 解析参数
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
// 根据 type 获取所有对应的处理过的 mutation 函数集合
const mutation = { type, payload }
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 执行 mutation 函数
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 执行所有的订阅者函数
this._subscribers.forEach(sub => sub(mutation, this.state))
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
// ...
梳理完 dispatch
和 commit
, 接着看后面的代码:
import devtoolPlugin from './plugins/devtool'
// ...
// 是否开启严格模式(true/false)
this.strict = strict
// 安装 modules
installModule(this, state, [], this._modules.root)
// 初始化 store._vm, 观测 state 和 getters 的变化
resetStoreVM(this, state)
// 安装插件
plugins.forEach(plugin => plugin(this))
if (Vue.config.devtools) {
devtoolPlugin(this)
}
// ...
后续的代码主要是安装 modules、vm 组件设置和安装通过 options
传入的插件以及根据 Vue 全局的 devtools
设置, 是否启用 devtoolPlugin
插件. 接下来就先分析下 vm 组件部分, 之后再分析安装 modules 的部分.
vm 组件设置
resetStoreVM
的定义如下:
function resetStoreVM (store, state, hot) {
// 旧的 vm 实例
const oldVm = store._vm
// 定义 getters 属性
store.getters = {}
// 获取处理的 getters 函数集合
const wrappedGetters = store._wrappedGetters
const computed = {}
// 循环所有处理过的getters,
// 并新建 computed 对象进行存储 getter 函数执行的结果,
// 然后通过Object.defineProperty方法为 getters 对象建立属性
// 使得我们通过 this.$store.getters.xxxgetter 能够访问到 store._vm[xxxgetters]
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
// 临时保存全局 Vue.config.silent 的配置
const silent = Vue.config.silent
// 将全局的silent设置为 true, 取消这个 _vm 的所有日志和警告
// in case the user has added some funky global mixins
Vue.config.silent = true
// 设置新的 vm, 传入 state
// 把 computed 对象作为 _vm 的 computed 属性, 这样就完成了 getters 的注册
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 还原 silent 设置
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
// 严格模式下, 在mutation之外的地方修改 state 会报错
enableStrictMode(store)
}
// 销毁旧的 vm 实例
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
module 安装
安装 modules
是 vuex 初始化的核心. ModuleCollection
方法把通过 options
传入的 modules
属性对其进行 Module
处理后, installModule
方法则会将处理过的 modules
进行注册和安装, 其定义如下:
// ...
installModule(this, state, [], this._modules.root)
//...
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// 设置上下文环境
const local = module.context = makeLocalContext(store, namespace, path)
// 注册 mutations
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 注册 actions
module.forEachAction((action, key) => {
const namespacedType = namespace + key
registerAction(store, namespacedType, action, local)
})
// 注册 getters
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)
})
}
// ...
installModule
接收5个参数: store
、rootState
、path
、module
、hot
. store
表示当前 Store
实例, rootState
表示根 state
, path
表示当前嵌套模块的路径数组, module
表示当前安装的模块, hot
当动态改变 modules
或者热更新的时候为 true
.
registerMutation
该方法是获取 store
中的对应 mutation type
的处理函数集合:
function registerMutation (store, type, handler, local) {
// 获取 type(module.mutations 的 key) 对应的 mutations, 没有就创建一个空数组
const entry = store._mutations[type] || (store._mutations[type] = [])
// push 处理过的 mutation handler
entry.push(function wrappedMutationHandler (payload) {
// 调用用户定义的 hanler, 并传入 state 和 payload 参数
handler.call(store, local.state, payload)
})
}
registerAction
该方法是对 store
的 action
的初始化:
function registerAction (store, type, handler, local) {
// 获取 type(module.actions 的 key) 对应的 actions, 没有就创建一个空数组
const entry = store._actions[type] || (store._actions[type] = [])
// push 处理过的 action handler
// 在组件中调用 action 则是调用 wrappedActionHandler
entry.push(function wrappedActionHandler (payload, cb) {
// 调用用户定义的 hanler, 并传入context对象、payload 参数和回调函数 cb
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
if (!isPromise(res)) {
// 将 res 包装为一个 promise
res = Promise.resolve(res)
}
// 当 devtools 开启的时候, 能捕获 promise 的报错
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
// 返回处理结果
return res
}
})
}
在调用用户定义的 action handler
时, 给改 handler
传入了三个参数: context 对象, payload 和一个回调函数(很少会用到). context
对象包括了 store
的 commit
和 dispatch
方法、当前模块的 getters
/state
和 rootState
/rootGetters
等属性, 这也是我们能在 action
中获取到 commit/dispatch
方法的原因.
registerGetter
该方法是对 store
的 getters
的初始化:
function registerGetter (store, type, rawGetter, local) {
// 根据 type(module.getters 的 key) 判断 getter 是否存在
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 包装 getter
// 在组件中调用 getter 则是调用 wrappedGetter
store._wrappedGetters[type] = function wrappedGetter (store) {
// 调用用户定义的 getter 函数
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
子 module 安装
注册完了根组件的 actions
、mutations
以及 getters
后, 递归调用自身, 为子组件注册其state
,actions
、mutations
以及 getters
等.
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
辅助函数
Vuex 除了提供我们 store
对象外, 还对外提供了一系列以 mapXXX
命名的辅助函数, 提供了操作 store
的各种属性的一系列语法糖. 辅助函数的定义均在 src/helpers.js
中, 由于 mapXXX
等函数的实现大同小异, 本文则只挑选常用的 mapActions
和 mapGetters
进行简单分析.
在分析之前, 先看两个函数的实现: normalizeNamespace
和 normalizeMap
.
//normalizeNamespace
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
// 如果传给 mapXXX 的第一个参数不是一个字符串
// 则将 namespace 赋值给 map 参数并将 namespace 设置为空
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
normalizeNamespace
函数的主要功能返回一个新的函数, 在新的函数中规范化 namespace
参数, 并调用函数参数fn
.
// normalizeMap
function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
normalizeMap
函数的作用则是将传递给 mapXXX
的参数统一转化为对象返回, 例如:
// normalize actions
normalizeMap(['test', 'test1']) ==> {test: 'test', val: 'test'}
// normalize getters
normalizeMap({
'test': 'getTestValue',
'test2': 'getTestValue2',
}) ==> {test: 'test', val: 'getTestValue'}
mapGetters
该函数会将 store
中的 getter
映射到局部计算属性中:
export const mapGetters = normalizeNamespace((namespace, getters) => {
// 返回结果
const res = {}
// 遍历规范化参数后的对象
// getters 就是传递给 mapGetters 的 map 对象或者数组
normalizeMap(getters).forEach(({ key, val }) => {
val = namespace + val
res[key] = function mappedGetter () {
// 一般不会传入 namespace 参数
if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
return
}
// 如果 getter 不存在则报错
if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
console.error(`[vuex] unknown getter: ${val}`)
return
}
// 返回 getter 值, store.getters 可见上文 resetStoreVM 的分析
return this.$store.getters[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
mapActions
该方法会将 store
中的 dispatch
方法映射到组件的 methods
中:
export const mapActions = normalizeNamespace((namespace, actions) => {
// 返回结果
const res = {}
// 遍历规范化参数后的对象
// actions 就是传递给 mapActions 的 map 对象或者数组
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// 保存 store dispatch 引用
let dispatch = this.$store.dispatch
if (namespace) {
// 根据 namespace 获取 module
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
// 绑定 module 上下文的 dispatch
dispatch = module.context.dispatch
}
// 调用 action 函数
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
总结
Vuex@2.4 的源码分析就暂时到这了. Vuex 的确是一个很强大的状态管理工具, 并且非常灵活, 但有一个问题就是, 如果严格按照单向数据流的方式进行开发, 并参考官方文档给予的项目目录结构, 随着应用的复杂度越来越高, 开发者会写非常多的模板类代码. 这个问题同样出现在 Redux, 因而在 Redux 社区出现了诸如 dva、mirror 这样的解决方案来减少模板类代码的开发, 提高开发效率; 同时, React 社区也出现了更轻巧的状态管理工具, 如statty. 而在 Vuex 社区, 貌似还没有出现的类似的解决方案(如果你知道, 还请 issue 留链接), 因而个人在阅读过 Vuex 的源码之后, 造了一些相关的轮子, 欢迎参考和使用:
当然, 也可以根据项目需要, 采用其它的状态管理方案, 例如 mobx.
感谢分享
确实dva将redux封装的更佳简化,通过reducer和effects实现非常强大的功能
这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this
你好,这里通过 this.$store来调用dispatch时,this肯定是指向 store的吧?
图片挂了