若川博客 Vuex 进阶:学习 vuex 源码整体架构,打造属于自己的状态管理库
【免费下载链接】blog若川的博客—撰写了学习源码整体架构系列几十篇。组织了源码共读活动,每周一起学习200行左右的源码,加我微信 ruochuan02 参与。项目地址: https://gitcode.com/gh_mirrors/blog51/blog
本文是「学习源码整体架构」系列的进阶篇,带你用断点调试 + 对象关系图的方式,完整看懂Vuex 源码整体架构:从Vue.use(Vuex)插件安装、new Vuex.Store构造函数,到state响应式原理与commit、dispatch等核心 API 的实现。掌握这套思路,你不仅能秒懂 Vuex 原理、应对面试,还能亲手打造属于自己的状态管理库。
想动手调试的读者,可以先克隆本仓库,完整源码解析都放在了docs/vuex/目录下(仓库地址:https://gitcode.com/gh_mirrors/blog51/blog),配合 vuex-store-instance.xmind 思维导图一起看,事半功倍。
一、为什么 Vuex 源码值得读:千行代码的状态管理库
Vuex 源码打包后只有一千多行,规模小、结构清晰,是学习状态管理原理的最佳入口 🚀。它的核心设计只有三点:
$store注入:通过全局Vue.mixin,在每个 Vue 实例的beforeCreate生命周期中注入同一个Store实例,所以组件里才能用this.$store.dispatch;state响应式:本质是用new Vue()包了一层私有实例,state挂在其_data.$$state上;getters即计算属性:把用户定义的 getters 注册为 computed,白嫖了 Vue 计算属性的懒加载缓存。
// 响应式的核心就这一句: store._vm = new Vue({ data: { $$state: state }, // 用户定义的 state computed // 处理后的 getters })也就是说,你在模板里写的$store.state.count,最终源码路径是:
vm.$store._vm._data.$$state.count二、Vue.use(Vuex) 插件安装:Vuex 对象结构一图看懂
先看Vuex对象的全貌,它本质是Vue 的一个插件,包含install、Store、mapState等 4 个辅助函数:
安装流程非常简单:
Vue.use(Vuex)触发install(_Vue)函数(vuex/src/store.js):记录Vue引用,然后调用applyMixin;applyMixin(vuex/src/mixin.js)执行Vue.mixin({ beforeCreate: vuexInit });vuexInit把根实例的store逐级向下注入,最终每个 Vue 实例都持有同一个Store实例:
function vuexInit () { const options = this.$options if (options.store) { this.$store = options.store } else if (options.parent && options.parent.$store) { this.$store = options.parent.$store } }💡 用购物车例子验证:vm.$store === vm.$children[0].$store结果为true,所有组件共享同一个 store。
三、new Vuex.Store 构造函数:记住三行核心代码
new Vuex.Store(options)之后,Store 实例长这样(这是全文最重要的一张图):
构造函数的执行流程可以概括为「三段式」:
第一段:条件断言。不满足直接抛错,这是 Vuex 报错信息的出处:
assert(Vue, 'must call Vue.use(Vuex) before creating a store instance.') assert(typeof Promise !== 'undefined', 'vuex requires a Promise polyfill...') assert(this instanceof Store, 'store must be called with the new operator.')第二段:声明内部变量。this._actions、this._mutations、this._wrappedGetters用于存放处理后的用户代码;this._modules = new ModuleCollection(options)构造模块树。这里用了Object.create(null)(无原型链的空对象)而非{},是个值得注意的细节。
第三段:三行核心代码,初读构造函数只需要盯住它们:
this._modules = new ModuleCollection(options) // 1. 收集模块,构造模块树 installModule(this, state, [], this._modules.root) // 2. 注册 state/mutation/action/getter resetStoreVM(this, state) // 3. new Vue 实现 state 响应式之后还有两个收尾动作:plugins.forEach(plugin => plugin(this))执行所有插件,以及按需初始化 vue-devtools 调试工具。
四、ModuleCollection:把模块变成一棵树
ModuleCollection(vuex/src/module-collection.js)负责把用户new Vuex.Store时传入的 options 加工成模块树,构造函数就一行:this.register([], rawRootModule, false),内部通过递归注册所有子模块。
每个节点都是一个Module实例(vuex/src/module.js),保存三样东西:
state:模块的状态(支持函数返回值或对象);_children:子模块;_rawModule:用户定义的原始模块。
以官方购物车示例(vuex/examples/shopping-cart)为例,store._modules最终长这样:
installModule函数则遍历这棵树,完成四件注册工作:
| 步骤 | 作用 |
|---|---|
| 注册 state | 非根模块通过Vue.set挂到父级 state 上,保证响应式 |
| 注册 mutation | 包一层wrappedMutationHandler,这就是 mutation 第一个参数是state的原因 |
| 注册 action | 包一层wrappedActionHandler,注入{ dispatch, commit, state, rootState }上下文,并自动 Promise 化 |
| 注册 getter | 包一层wrappedGetter,收集到_wrappedGetters |
五、resetStoreVM:Vuex state 响应式原理全解
这是 Vuex 最巧妙的地方——用一个隐藏的 Vue 实例承载响应式:
store._vm = new Vue({ data: { $$state: state }, // state 树变成响应式 computed // getters 变成计算属性 })细节有三个:
- getters 绑定:把
_wrappedGetters中每一项包成computed(借助partial函数避免闭包持有旧 vm),再通过Object.defineProperty暴露到store.getters上,懒加载 + 缓存白嫖 Vue 计算属性机制; - 静默模式:
new Vue前临时打开Vue.config.silent,避免这个隐藏实例触发全局 mixin 的警告; - 严格模式:若
strict: true,调用enableStrictMode用$watch深度观测 state——没走_withCommit的修改都会报警,这就是 Vuex 强制「只能通过 mutation 改 state」的实现原理。
六、Vuex 核心 API:commit 与 dispatch 的实现
Store 原型上还有commit、dispatch、replaceState、watch、subscribe、registerModule等 API,核心两个的实现思路如下:
commit(提交 mutation):
commit (_type, _payload, _options) { const { type, payload } = unifyObjectStyle(_type, _payload, _options) const entry = this._mutations[type] this._withCommit(() => { // 标记「正在合法修改」 entry.forEach(handler => handler(payload)) }) this._subscribers.forEach(sub => sub(mutation, this.state)) // 触发订阅 }dispatch(分发 action):取出_actions[type]中的处理器逐个执行,结果自动 Promise 化,并在执行前后触发before/after订阅者(subscribeAction注册的),这也是 Vuex 支持异步操作的根基。
另外store.watch是拿另一个隐藏实例_watcherVM.$watch实现的;registerModule/unregisterModule则演示了动态模块的完整生命周期。
七、mapState 等辅助函数:组件绑定的「语法糖」
mapState、mapGetters、mapMutations、mapActions都位于vuex/src/helpers.js,实现套路高度统一:
normalizeNamespace统一命名空间(没传就置空,末尾补/);normalizeMap把数组/对象统一成[{ key, val }]数组;- 为每一项生成一个计算属性或方法,从
this.$store(或命名空间模块的context)取值。
例如mapState在无命名空间时:
// 你写的 ...mapState({ checkoutStatus: state => state.cart.checkoutStatus }) // 等价于 checkoutStatus: function () { return this.$store.state.checkoutStatus }而createNamespacedHelpers('cart')则是把这四个函数bind上命名空间,方便命名空间模块集中使用。这些辅助函数配合第四节的module.context(makeLocalContext生成),彻底抹平了「是否使用命名空间」的差异 ✨。
八、总结:动手调试,打造你自己的状态管理库
回顾一下 Vuex 源码整体架构的完整链路:
Vue.use(Vuex)安装插件 →new Vuex.Store断言 + 内部变量初始化 →ModuleCollection构造模块树 →installModule注册 state/mutation/action/getter →resetStoreVM用new Vue实现响应式 →commit/dispatch/watch等 API 围绕store._vm服务 →mapState等辅助函数抹平命名空间差异。
几个调试建议(来自作者的实战经验):
- 先看官方 Shopping Cart 例子(
vuex/examples/shopping-cart),把store实例在控制台打印出来对照关系图看; - 给 webpack 配置加上
devtool: 'source-map',然后在 Chrome 断点调试installModule、resetStoreVM两个函数即可串起主线; - 会调试代码,看源码就简单了——不调试只看文章,很难消化吸收。
Vuex 源码虽然只有千行出头,但把「响应式、模块化、插件化、订阅者模式」这些状态管理库的通用设计全用到了。读懂它之后,再去看本仓库中 Vuex 4(Vue 3 下基于provide/inject的docs/vuex4/README.md)、Redux(docs/redux/README.md)等系列分析,你会发现它们不过是同一套架构的不同变体。动手照着本文的链路写一个玩具版状态管理库,你离「源码大神」又近了一步 💪。
【免费下载链接】blog若川的博客—撰写了学习源码整体架构系列几十篇。组织了源码共读活动,每周一起学习200行左右的源码,加我微信 ruochuan02 参与。项目地址: https://gitcode.com/gh_mirrors/blog51/blog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考