Vuex 源码学习
1. 我的疑问
1.1 $store 是怎么保证每个组件都可以进行访问的
1.2. 为什么需要 Mutation,而不是直接使用 Action
1.3. mapState 等方法是怎么映射到组件的
1.4. 怎么和 Vue 的响应式结合起来的
1.5. 插件的实现【兴趣不大】
1.6. 模块的实现【兴趣不大】
带着我的疑问,现在就开搞!
2. 基本介绍
Vuex
的核心就是一个 Store
, 管理着应用的全局状态。组件从 store
中读取数据,当 store
中数据变化时,组件也会一起更新。Vuex
中的数据需要通过 Mutation
进行显示更改,当然根据实际业务直接更改对象类型的 state
其实也可以。显示更改更方便我们定位问题和在 devtools
中进行展示
项目地址: https://github.com/vuejs/vuex
构建工具: Rollup
入口文件:src/index.js
由于平时使用的 3.x 版本的 Vuex
,所以分支为 v3.6.2
3. 初始化
回想下平时我们是怎么使用 Vuex, 我们使用 Vue.use
安装了 Vuex
插件,然后通过
然后再来看 Vuex
导出的相关包
Vue.use(Vuex); const store = new Vuex.Store({ state, getters, mutations, actions, }) export default store;
首先我们查看入口文件文件
export default { Store, install, version: '__VERSION__', mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers, createLogger }
Store
就是我们初始化的类, install
就是 Vue.use
安装的逻辑,还有平时使用到的一些辅助函数。
3.1 Vuex 初始化
看下 install
的逻辑
export function install () { 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
中混入一些逻辑,2.0 中混入了 beforeCreate
函数的逻辑
Vue.mixin({ beforeCreate: vuexInit })
而 vuexInit
中的逻辑就是混入 $store
, 保证应用下的组件都可以访问到 $store
。
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 } }
通过这里的逻辑就能解答我的[第一个疑问]了
3.2 Store 初始化
Store 初始化主要分为:
- Module 注册,处理嵌套逻辑,形成 tree
- Module 安装,初始化 state,getters 等
- 初始化 Store.vm
3.2.1 Module 注册
初始化 Store
时我们会传入一个对象,里面包含 state
, mutations
, getters
等属性。现在我们就来看看 Store
这个类.
this._modules = new ModuleCollection(options)
整个 options
可以视为 Store 的 root module, 在 ModuleCollection 中进行解析,如果有嵌套的 module ,该方法也会进行处理
看一下模块注册的逻辑
register (path, rawModule, runtime = true) { const newModule = new Module(rawModule, runtime) // 构建 tree if (path.length === 0) { this.root = newModule } else { // slice(0, -1) 不包括最后一项 const parent = this.get(path.slice(0, -1)) parent.addChild(path[path.length - 1], newModule) } // 处理嵌套的模块 if (rawModule.modules) { forEachValue(rawModule.modules, (rawChildModule, key) => { this.register(path.concat(key), rawChildModule, runtime) }) } }
其中 path
为 store tree
的路径, rawModule
为配置项, runtime
为是否运行时创建的模块。
通过 Module 类中对模块进行初始化,判断当前 path
的长度,若为 0 则称为 root module
, 否则就找到 父模块调用 addChild
方法形成父子逻辑。
这样子模块可以通过路径找到父模块,父模块可以通过 _children
key
找到子模块
3.2.2 Module 安装
installModule(this, state, [], this._modules.root)
这个方法的核心逻辑为
// 获取命名空间,模块标识 namespace 为 true, 可以嵌套 const namespace = store._modules.getNamespace(path) 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) }) } // 挂载 state const local = module.context = makeLocalContext(store, namespace, path) // 处理 mutations,action,getter module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) // 嵌套安装 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })
在 makeLocalContext
方法中实现上下文的注册,上下文包括了 state
, getters
, dispatch
, commit
等。这几个属性除了 state
都处理了 namespace
的情况。这里讲一下 state
的获取逻辑就好了
Object.defineProperties(local, { getters: { get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace) }, state: { get: () => getNestedState(store.state, path) } }) function getNestedState (state, path) { return path.reduce((state, key) => state[key], state) }
从 state
开始,层层查找子模块 state
,最终找到目标模块的 state
.
registerMutation
, registerAction
等方法完成对 mutation
, action
的注册
3.2.3 初始化 Store.vm
resetStoreVM(this, state)
resetStoreVM 的具体逻辑是这样的
function resetStoreVM (store, state, hot) { const oldVm = store._vm // bind store public getters store.getters = {} // reset local getters cache store._makeLocalGettersCache = Object.create(null) const wrappedGetters = store._wrappedGetters const computed = {} forEachValue(wrappedGetters, (fn, key) => { computed[key] = partial(fn, store) Object.defineProperty(store.getters, key, { get: () => store._vm[key], enumerable: true // for local getters }) }) const silent = Vue.config.silent Vue.config.silent = true store._vm = new Vue({ data: { $$state: state }, computed }) Vue.config.silent = silent if (oldVm) { if (hot) { store._withCommit(() => { oldVm._data.$$state = null }) } Vue.nextTick(() => oldVm.$destroy()) } }
这里的流程就是从 _wrappedGetters
中取出对应 gettter
函数,挂在到 store._vm
中,同时存入 computed
中,通过 Vue
的 compute
,和 state
产生依赖关系
store._vm = new Vue({
data: {
$$state: state
},
computed
})
然后 store
的 state
的 getter
是这么设置的
get state () { return this._vm._data.$$state }
因此获取 state
时,实际上访问的是 vm 的 $$state
总体的逻辑来说就是这样的
根据 `key` 访问 `store.getters` 的某一个 `getter` 的时候,实际上就是访问了 `store._vm[key]`,也就是 `computed[key]`,在执行 `computed[key]` 对应的函数的时候,会执行 `rawGetter(local.state,...)` 方法,那么就会访问到 `store.state`,进而访问到 `store._vm._data.$$state`,这样就建立了一个依赖关系。当 `store.state` 发生变化的时候,下一次再访问 `store.getters` 的时候会重新计算
在上面的代码中还有 严格模式的校验
function enableStrictMode (store) { store._vm.$watch(function () { return this._data.$$state }, () => { if (__DEV__) { assert(store._committing, `do not mutate vuex store state outside mutation handlers.`) } }, { deep: true, sync: true }) }
如果处于严格模式,就会手动添加一个 同步的 watch
, 监听 $$state
的变化是否是 mutation
改变的,
_withCommit (fn) { const committing = this._committing this._committing = true fn() this._committing = committing }
_withCommit 方法保证了通过 Vuex 自身提供的方法改变数据才能使 _committing
为 true
读到这里,关于 [1.4]和[1.6]的问题,已经有了答案,响应式是通过 Vue
实现的,模块是 Store
的基本单位,兼容层级的嵌套和命名空间。
4. API
在 store 的初始化过程中完成了数据的存储,接下来就是通过 API 来进行使用了。
4.1 Mutation & commit
在初始化时,我们进行了 mutation 方法的初始化,可以通过 mutation 进行 state 的更改。
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
中改变的数据是当前模块的数据,访问的 this
是 store
. 然后将每个 mutation
存入到 store._mutations[type]
中
然后我们再看看 commit, store 提供 commit 来提交 commit
commit (_type, _payload, _options) { // 规范传入的参数 const { type, payload, options } = unifyObjectStyle(_type, _payload, _options) const mutation = { type, payload } // 获取存放的方法 const entry = this._mutations[type] // 执行 mutation 改变 state this._withCommit(() => { entry.forEach(function commitIterator (handler) { handler(payload) }) }) // 提供给订阅者使用,一般是插件 this._subscribers .slice() .forEach(sub => sub(mutation, this.state)) }
从这里可以看出,mutation 必须是同步的,不然下面通知 订阅者 的逻辑会出错
4.2 Action & dispatch
Action 和 Mutation 的区别就是 Action 是异步的,所以它们大部分逻辑还是差不多的,只是在异步处理和通知上有区别
... // 执行前 hook try { this._actionSubscribers .slice() .filter(sub => sub.before) .forEach(sub => sub.before(action, this.state)) } catch (e) { } const result = entry.length > 1 ? Promise.all(entry.map(handler => handler(payload))) : entry[0](payload) return new Promise((resolve, reject) => { // 完成 hook 和 error hook result.then(res => { this._actionSubscribers .filter(sub => sub.after) .forEach(sub => sub.after(action, this.state)) resolve(res) }, error => { this._actionSubscribers .filter(sub => sub.error) .forEach(sub => sub.error(action, this.state, error)) reject(error) }) })
4.2 mapState
在 src/helper.js
中
export const mapState = normalizeNamespace((namespace, states) => { const res = {} normalizeMap(states).forEach(({ key, val }) => { // 组成 getter 函数 res[key] = function mappedState () { let state = this.$store.state let getters = this.$store.getters // 有命名空间就使用模块的数据,不然就使用根的 // 这里的 this 是指向组件的 if (namespace) { const module = getModuleByNamespace(this.$store, 'mapState',namespace) if (!module) { return } state = module.context.state getters = module.context.getters } return typeof val === 'function' ? val.call(this, state, getters): state[val] } // mark vuex getter for devtools res[key].vuex = true }) return res })
这里的主要逻辑是 得到一些 getter 函数,其中也包括对一些命名空间的处理。这里兼容了 val 是函数还是字符串的情况。
接下来我们看看 normalizeNamesapce
函数干了啥,
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/state
做处理,拿到斜杠前的命名空间 然后对函数进行包裹,兼容传命名空间和不穿命名空间的两种情况。
4.3 mapMutations
其实 mapMutations
的逻辑和 mapState
差不多,这里讲一下核心的逻辑就好了
res[key] = function mappedMutation (...args) { let commit = this.$store.commit // 没 namespace 就用 store 的顶级 commit,不然就使用模块上下文自己的 commit if (namespace) { const module = getModuleByNamespace(this.$store, 'mapMutations', namespace) if (!module) return commit = module.context.commit } // 然后执行 commit 函数,也支持 val 是函数的情况,将 commit 进行传入 return typeof val === 'function' ? val.apply(this, [commit].concat(args)) : commit.apply(this.$store, [val].concat(args)) }
其他的 mapGetters
, mapActions
的逻辑都与其类似。
5. 插件
插件平时用的还是很少的,也就学习教程时知道一个 logger
插件,还有 Vuex 持久化
的插件。
现在就顺便研究下插件机制, 显示插件激活
plugins.forEach(plugin => plugin(this))
代码其实就是将 store
传递给插件,然后插件调用 store.subscribe
订阅 mutation
事件,或者订阅 action
事件。
看了这么多源码,想一下 持久化插件会怎么做呢?我觉得可以在每次数据改变后 将当前 state
存到 localStorage
中,当 store
在激活插件时,从 localStorage
中拿到旧的数据,然后改变 $$state
看了下源码果然逻辑差不多 Vuex-persistedstate
6. 总结
看完源码后我的 6 个疑问解决了 5 个,收获还是很大的。对于自己平时的开发,如果状态比较多的话,子模块和命名空间的确很好用。毕竟 Vuex 花了很大精力去处理这块逻辑。
还有插件的设计,也让我学会了如何去制作 Vuex 插件和 设计插件系统,当然还有最后的疑问,为啥需要 Mutation,而不是直接使用 Action 呢?像 pinia 中就放弃了 Mutation.
我其实也有了答案,正如作者说的,Mutation 其实是为了更好的追踪状态的改变,方便 devtools 去捕捉,同时数据更改后能够及时同步插件,保证插件获取的数据是正确的。
当然直接使用 Action 也是可以,因为 Action 也能够通知订阅者。且用户直接更改数据也不会影响响应式的功能。所以这也是为什么 Mutation 被放弃的原因。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论