Vuex 浅析
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
在 Vue 项目中引入 Vuex,需要采用插件引入方式。Vue.use(Vuex)
安装 Vuex 插件,会调用插件中提供的 install 方法
// src/stiore.js
// store.js 的顶部定义了一个 Vue 变量,防止注册多次
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
//这里调用 了 applyMixin 看名字应该是个混入,混入啥呢???
}
applyMixin 方法的主要功能将初始化 Vue 实例时传入的 store 设置到 this 对象的 $store 属性上, 子组件则从其父组件引用$store 属性, 层层嵌套进行设置. 这样, 任何一个组件都能通过 this.$store 的方式访问 store 对象了.
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
//// 2.x 通过 hook 的方式注入
// Vue 全局混入一个混入对象,在之后每个创建的 Vue 实例的 beforeCreate 生命周期中添加 vuexInit 函数
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
// 使用自定义的 _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)
}
}
function vuexInit () {
const options = this.$options
// 创建 Vue 根实例的 option
// store injection 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
}
}
}
在 Vue 项目中,new 一个 Store 实例,并在创建 Vue 根实例时传入 store 实例
const store = new Vuex.Store({
state,
getters,
mutations,
actions,
modules: {
...
},
});
new Vue({
store
...
})
接下来 Store 的构造函数
1、vuex 先对构造 store 需要的一些环境变量进行断言:
if (!Vue && typeof window !== 'undefined' && window.Vue) {
// Vue 是全局变量时,自动 install
install(window.Vue)
}
if (process.env.NODE_ENV !== 'production') {
// 在非生产环境下,进行一些断言
// 当不满足参数 1 为 false 时抛出错误
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.`)
}
2、初始化变量
// store internal state
// 是否在进行提交状态标识
this._committing = false
this._actions = Object.create(null)
// 监听所有的 action
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
// 监听所有的 mutation
this._subscribers = []
//创建一个 Vue 实例, 利用 $watch 监测 store 数据的变化
this._watcherVM = new Vue()
重点看下 this._modules = new ModuleCollection(options)
收集 modules 会调用 ModuleCollection,optinons 为 Store 构造函数传入的参数。从函数命名可以看出是模块收集注册的。
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
//注册根 module
this.register([], rawRootModule, false)
}
...
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
// 检查模块内的 getters/ mutations/ actions 可迭代且是函数 或者 actions 的存在 handler 属性且属性值为函数
assertRawModule(path, rawModule)
}
// 创建 module 对象
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// root 保存着根 module
this.root = newModule
} else {
// 简单说 确定模块的父子关系
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
// parent 为父 module,在 parent._children[path] = newModule
}
// register nested modules
// 递归创建子 module
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
...
}
forEachValue 是一个工具函数,对对象中的每个键值对调用函数
/**
* forEach for object
*/
export function forEachValue (obj, fn) {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
ModuleCollection 主要将传入的 options 对象整个构造为一个 module 对象, 并循环调用 register 为其中的 modules 属性进行模块注册, 使其都成为 module 对象, 最后 options 对象被构造成一个完整的组件树 。ModuleCollection new Module(rawModule, runtime)
来创建具体的 module。
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null)
// Store the origin module object which passed by programmer
this._rawModule = rawModule
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
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、把 Store 类的 dispatch 和 commit 的方法的 this 指针指向当前 store 的实例上. 这样做的目的可以保证当我们在组件中通过 this.$store 直接调用 dispatch/commit 方法时, 能够使 dispatch/commit 方法中的 this 指向当前的 store 对象而不是当前组件的 this.
// 绑定 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)
}
dispatch 的功能是触发并传递一些参数(payload)给与 type 对应的 action
commit 的功能是触发并传递一些参数(payload)给与 type 对应的 mutation
4、store 其他重要属性的配置
// 确保 dispatch/commit 方法中的 this 对象正确指向 store
this.strict = strict
// 根 module 的 state
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
// installModule 方法则会将处理过的 modules 进行注册和安装,
/// installModule 接收 5 个参数: store、rootState、path、module、hot.
// store 表示当前 Store 实例, rootState 表示根 state, path 表示当前嵌套模块的路径数组
// module 表示当前安装的模块,
// hot 当动态改变 modules 或者热更新的时候为 true
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
// 根据 path 或者路径上各层模块的 namespace
const namespace = store._modules.getNamespace(path)
// 根据 path 数组 寻找路径上的模块,把各模块的 namespaced 拼接起来
// register in namespace map
// 在 store 上的 _modulesNamespaceMap 注册有 namesoaced 的模块
// 方便后面根据 namespace 属性 直接获取模块
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// set state
// 当 !isRoot 为 true 说明 path.length > 0
// 当 !hot 为 true,说明 hot 为 false
if (!isRoot && !hot) {
// 根据 path 找到父级模块
// 可知,在父 module 的 state 中通过 path 路径名注册子 state
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// 给模块绑定上下文在 mapActions 等辅助函数时有用
// 该方法其实是在重写 dispatch 和 commit 函数
// 你是否有疑问模块中的 dispatch 和 commit
// 是如何找到对应模块中的函数的
// 假如模块 A 中有一个名为 add 的 mutation
// 通过 makeLocalContext 函数,会将 add 变成
// a/add,这样就可以找到模块 A 中对应函数了
const local = module.context = makeLocalContext(store, namespace, path)
//给模块上下文绑定处理过 type 的 dispatch,mutation,getters,
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
//向 store._mutations[type] 数组添加处理函数
// local 传递,保证 mutation 的第一个参数是模块内部的 state === local.state
registerMutation(store, namespacedType, mutation, local) // store._mutations[type] type 为 namespacedType
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
// local 传递 local.dispatch,local.commit,local.getters,local.state
// type 作用只为找到 store._actions[type] 数组 添加处理函数
registerAction(store, type, handler, local)
// store._actions[type] type 为 namespacedType
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
// 在 store._wrappedGetters[type] (type 为 namespacedType)
// 向 module 内部的 getter 传递 local.state,local.getters, root state, root getters,
})
// 循环注册子模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
简单提下如何模块的 context 是怎么处理的吧
function makeLocalContext (store, namespace, path) {
// 是否存在命名空间
const noNamespace = namespace === ''
const local = {
// 根 module 与子 module 都是调用 store.dispatch
// 子 module 的 type 会经过命名空间处理
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 = namespace + type
// 非生产环境且 actions[type] 不存在
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
return store.dispatch(type, payload)
},
// 根 module 与子 module 都是调用 store.commit
// 子 module 的 type 会经过命名空间处理
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 (process.env.NODE_ENV !== 'production' && !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
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace) //返回一个新的对象,属性是 localType,value 是 store.getters[namespacedType]
},
state: {
get: () => getNestedState(store.state, path)
// 根据 path 数据返回模块内部的 state
// 在根 state 上一层层取值获取
}
})
return local
}
5、上面可以看到有个函数 resetStoreVM(this, state)
,原注释是初始化 store vm,利用 Vue 的数据响应系统来监听内部变化,同时让 store.__wrappedGetters 变成绑定成 vm 的计算属性,响应变化。
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
// bind store public getters
//定义 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
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(fn, store)
// 等价于 computed[key] = () => fn(store)
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
const silent = Vue.config.silent // 先暂存用户配置
Vue.config.silent = true // 取消 Vue 的所有日志与警告
// 设置新的 vm, 传入 state
// 把 computed 对象作为 _vm 的 computed 属性, 这样就完成了 getters 的注册
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent // 恢复用户配置
// enable strict mode for new vm
if (store.strict) {
// 严格模式下, 在 mutation 之外的地方修改 state 会报错
enableStrictMode(store)
}
// 销毁旧的 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())
}
}
上文提到严格模式,是如何控制严格模式的呢
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 })
// 利用 store 上的 _vm 属性指向的 vue 实例的 watch 时刻观察 store.state 变化。
// 回调函数为断言 store._committing 是否为 true,为 true 说明是 mutation
}
6、了解 vuex 的文档,可知 vuex 提供订阅 mutation 和 action 的功能,这又是如何实现的?可以自主想想,是不是可以在每次调用 mutiaon,action 时 回调你的 handler 呢?来看下 commit 和 dispathc 里面的处理吧
在 store 的属性配置里初始化了
// 监听所有的 action
this._actionSubscribers = []
// 监听所有的 mutation
this._subscribers = []
// 订阅 mutation
subscribe (fn) {
return genericSubscribe(fn, this._subscribers)
}
//订阅 store 的 action。
// handler 会在每个 action 分发的时候调用并接收
// action 描述和当前的 store 的 state 这两个参数:
subscribeAction (fn) {
// 默认是 before
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers)
}
// 没有就添加 有就删除
function genericSubscribe (fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
// 目前可知 mutation 订阅时往 this._subscribers 数组中 push 处理函数,action 订阅时往 this._actionSubscribers 数组中 push 处理函数
// 接下来简单看下 commit 时处理触发订阅呢
commit (_type, _payload, _options) {
// check object-style commit
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
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
}
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 (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
// 调用 before 时机的处理函数
// 在 before 和 after 不存在的情况默认是 before
try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
const result = entry.length > 1
// handler 返回的肯定是一个 Promise
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 调用 after 时机的处理函数
return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
}
其实还有很多没提,比如 mapXXX 等辅助函数是如何实现,如果动态添加插件,这些大家有兴趣可以去探索呢
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Async 函数说明
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论