Vuex
源码不过千行,主要使用了 Store
类、ModuleCollection
类、Module
类,结构清晰,下面简单说说 Vuex
中一些主要的源码实现。推荐打开 Vuex
源码一同观看。?
vuex使用方法
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
new Vue({
store,
...
})
按照使用方法,依次看看 vuex
做了什么事情。 ?
Vue.use(Vuex)
首先,装载Vuex插件,Vue.use 方法会调用传入 plugin 的 install 方法。在源码 src/index.js 中发现了 install
方法
import { Store, install } from './store'
install
函数调用了核心方法 applyMixin(Vue)
,applyMixin
位于 src/mixin.js
文件,
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
...
}
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
}
}
}
其核心作用在于把我们传入的store
对象,挂载到后代的每个 Vue
实例上,让我们可以通过this.$store
访问store
对象。?
new Vuex.Store()
接下来就是核心部分,实例化 Store
。看 constructor
函数运行发生了什么。
...
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
...
首先定义了一堆的属性,重点的一行是 this._modules = new ModuleCollection(options)
。
ModuleCollection 类就是一个 module
管理中心,负责注册、删除、标识根 module
等简单功能,options
是实例化Store
时传入的参数。module
的定义如下
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store
对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将
store
分割成模块(module)
。每个模块拥有自己的state
、mutation
、action
、getter
、甚至是嵌套子模块——从上至下进行同样方式的分割
ModuleCollection类
ModuleCollection
类的构造函数如下
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
...
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
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)
}
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
...
}
register
方法中,path
参数是为了实现命名空间的功能,保存了module
的结构化信息。暂且不看new Module
的逻辑。if (path.length === 0)
因为构造函数传入的path为[]
,所以这个判断条件为ture,标识了root module
。 接着一个递归判断 if (rawModule.modules)
如果有 modules
属性,就重复调用 register
方法,path
参数则加上了当前的 module
名。
如果我们传入的option 是这样
{
modules: {
a: {...}
}
}
递归的时候 path
就成了 ['a']
这样了,为什么要记录 path
呢。
默认情况下,模块内部的action
、mutation
和getter
是注册在全局命名空间的——这样使得多个模块能够对同一mutation
或action
作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有
getter
、action
及mutation
都会自动根据模块注册的路径调整命名。
一旦一个模块使用了命名空间功能,我们就必须要知道模块的注册路径,才方便在用户写下 this.$store.a.b.c.state
时,去查找对应模块的属性 。
Module类
Module
类的构造函数十分简单。只是简单的储存了传入的 new Store
时的 option
,并且提供了子模块的管理、重写 actions
、mutations
方法功能。
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) || {}
}
...
}
ok 属性初始化完成。接着继续看 Store
的构造函数方法。
// bind commit and dispatch to self
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)
}
重载了一下 dispatch
方法 和 commit
方法,让它们的 this
变成强绑定,免得发生 this
丢失对的情况。
假如不显示绑定,在这种情况下,this
指向就变化了
a = store.commit
a('xx') // error , this指向全局了
在看核心代码 installModule
函数
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
installModule
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
重复的模块名称报错
}
store._modulesNamespaceMap[namespace] = module
}
...
const local = module.context = makeLocalContext(store, namespace, path)
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
...
makeLocalContext
函数主要为了抹平有命名空间的情况下访问 commit
、dispatch
、store
的差异化,自动在调用时加上 namespace
的路径。代码如下?
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
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
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
未定义时报错
}
}
return store.dispatch(type, payload)
},
commit: ...
}
// 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)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
接下来则是把定义的 mutation
、action
、getter
挂载到 store
实例对象上。以 mutation
举例
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
我们看得,存储 mutation
的数据类型是一个数组,也就是说可以定义多个同名 的 mutation
,Vuex
都会记录下来,是否调用我们可以在 commit
方法看到。?
resetStoreVM
以上 store
对象就基本处理完成了,剩下最后一部,把 state
变成响应式对象,使得我们在改变 state
的时候,触发对应的副作用,比如 getters
的值更新,比如触发 watch
的回调。核心代码如下:
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vuex
直接使用了 Vue
实例绑定。computed
对象就是我们定义的 getter
包装而来的。
以上就是 new Store(option)
中所执行的所有代码了。
最后再看看我们用的最多的 commit
方法。?
commit
commit (_type, _payload, _options) {
...
const mutation = { type, payload }
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
}
获取 type
然后在 _withCommit
里面执行匹配到的mutation。这里也印证了我们上文的猜想,可以注册多个同名mutation
。
_withCommit
的方法很简单。?
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
保存了 committing 之前状态,然后把 _committing
设置为 true
, 执行完 commit
的 fn
后,在还原,在严格模式下,state 的写操作会判定 _committing 的状态,确保只有 commit
能修改 state
。
最后还调用了 this._subscribers, 看了文档发现对应的功能如下。
subscribe(handler: Function): Function
订阅
store
的mutation
。handler
会在每个 mutation 完成后调用,接收mutation
和经过mutation
后的状态作为参数
另外,这段代码有点与众不同, this._subscribers.slice().forEach
,所有订阅函数经过了一次浅拷贝。防止在订阅回调中同步取消订阅,修改了this._subscribers
长度,导致forEach
次数变少,issue。
在执行一些回调的时候一定要多考虑回调可能带来的副作用。?
# 完
由于水平有限,以上解读不保证100%✅,尽量依照源码观看,如有错误欢迎指正。
通篇浏览,代码过多,以后还是写一些小点?。阅读源码本来之目的是为了学习作者的代码设计、理解,比如 Vuex
对于命名空间、module
的初始化、分割管理操作等,类的设计抽象等等。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。