前言
你好,我是若川。这是
学习源码整体架构
第五篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。
学习源码整体架构
系列文章如下:
1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
感兴趣的读者可以点击阅读。下一篇可能是学习 axios
源码。
导读
文章比较详细的介绍了vuex
、vue
源码调试方法和 Vuex
原理。并且详细介绍了 Vuex.use
安装和 new Vuex.Store
初始化、Vuex.Store
的全部API
(如dispatch
、commit
等)的实现和辅助函数 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。
chrome 浏览器调试 vuex 源码方法
从上文中同理可得调试 vuex
方法,这里详细说下,便于帮助到可能不知道如何调试源码的读者。
可以把笔者的这个 vuex-analysis 源码分析仓库fork
一份或者直接克隆下来,git clone https://github.com/lxchuan12/vuex-analysis.git
其中文件夹
vuex
,是克隆官方的vuex
仓库dev
分支。截至目前(2019年11月),版本是
v3.1.2
,最后一次commit
是ba2ff3a3
,2019-11-11 11:51 Ben Hutton
。包含笔者的注释,便于理解。
克隆完成后, 在vuex/examples/webpack.config.js
中添加devtool
配置。
1 | 复制代码// 新增devtool配置,便于调试 |
1 | 复制代码git clone https://github.com/lxchuan12/vuex-analysis.git |
点击你想打开的例子,例如:Shopping Cart => http://localhost:8080/shopping-cart/
打开控制面板 source 在左侧找到 webapck// . src 目录 store 文件 根据自己需求断点调试即可。
本文主要就是通过Shopping Cart
,(路径vuex/examples/shopping-cart
)例子调试代码的。
顺便提一下调试 vue 源码(v2.6.10)的方法
1 | 复制代码git clone https://github.com/vuejs/vue.git |
克隆下来后将package.json
文件中的script
dev
命令后面添加这个 --sourcemap
。
1 | 复制代码{ |
1 | 复制代码git clone https://github.com/vuejs/vue.git |
本小节大篇幅介绍调试方法。是因为真的很重要。会调试代码,看源码就比较简单了。关注主线调试代码,很容易看懂。
强烈建议克隆笔者的这个仓库,自己调试代码,对着注释看,不调试代码,只看文章不容易吸收消化。
笔者也看了文章末尾笔者推荐阅读的文章,但还是需要自己看源代码,才知道这些文章哪里写到了,哪里没有细写。
正文开始~
vuex 原理
简单说明下 vuex
原理
1 | 复制代码<template> |
每个组件(也就是Vue实例
)在beforeCreate
的生命周期中都混入(Vue.mixin)同一个Store实例
作为属性 $store
,
也就是为啥可以通过 this.$store.dispatch
等调用方法的原因。
最后显示在模板里的$store.state.count
源码是这样的。
1 | 复制代码class Store{ |
其实就是:vm.$store._vm._data.?state.count
其中vm.$store._vm._data.?state
是 响应式的。
怎么实现响应式的?其实就是new Vue()
1 | 复制代码function resetStoreVM (store, state, hot) { |
这里的 state
就是 用户定义的 state
。
这里的 computed
就是处理后的用户定义的 getters
。
而 class Store
上的一些函数(API)主要都是围绕修改vm.$store._vm._data.?state
和computed(getter)
服务的。
Vue.use 安装
笔者画了一张图表示下Vuex
对象,是Vue
的一个插件。
Vuex 对象关系图
看到这里,恭喜你已经了解了
Vuex
原理。文章比较长,如果暂时不想关注源码细节,可以克隆一下本仓库代码git clone https://github.com/lxchuan12/vuex-analysis.git
,后续调试代码,点赞收藏到时想看了再看。
文档 Vue.useVue.use(Vuex)
参数:
Object Function plugin
用法:安装 Vue.js 插件。如果插件是一个对象,必须提供
install
方法。如果插件是一个函数,它会被作为install
方法。install
方法调用时,会将 Vue 作为参数传入。该方法需要在调用
new Vue()
之前被调用。当
install
方法被同一个插件多次调用,插件将只会被安装一次。
根据断点调试,来看下Vue.use
的源码。
1 | 复制代码function initUse (Vue) { |
install 函数
vuex/src/store.js
1 | 复制代码export function install (_Vue) { |
接下来看 applyMixin
函数
applyMixin 函数
vuex/src/mixin.js
1 | 复制代码export default function (Vue) { |
最终每个Vue
的实例对象,都有一个$store
属性。且是同一个Store
实例。
用购物车的例子来举例就是:
1 | 复制代码const vm = new Vue({ |
Vuex.Store 构造函数
先看最终 new Vuex.Store
之后的 Store
实例对象关系图:先大致有个印象。
1 | 复制代码export class Store { |
1 | 复制代码if (!Vue && typeof window !== 'undefined' && window.Vue) { |
如果是 cdn script
方式引入vuex
插件,则自动安装vuex
插件,不需要用Vue.use(Vuex)
来安装。
1 | 复制代码// asset 函数实现 |
1 | 复制代码if (process.env.NODE_ENV !== 'production') { |
条件断言:不满足直接抛出错误
1.必须使用
Vue.use(Vuex)
创建store
实例。2.当前环境不支持
Promise
,报错:vuex
需要Promise polyfill
。3.
Store
函数必须使用new
操作符调用。
1 | 复制代码const { |
从用户定义的new Vuex.Store(options)
取出plugins
和strict
参数。
1 | 复制代码// store internal state |
声明Store
实例对象一些内部变量。用于存放处理后用户自定义的actions
、mutations
、getters
等变量。
提一下
Object.create(null)
和{}
的区别。前者没有原型链,后者有。
即Object.create(null).__proto__
是undefined
({}).__proto__
是Object.prototype
1 | 复制代码// bind commit and dispatch to self |
给自己 绑定 commit
和 dispatch
为何要这样绑定 ?
说明调用
commit
和dispach
的this
不一定是store
实例这是确保这两个函数里的
this
是store
实例
1 | 复制代码// 严格模式,默认是false |
上述这段代码installModule(this, state, [], this._modules.root)
初始化 根模块。
并且也递归的注册所有子模块。
并且收集所有模块的
getters
放在this._wrappedGetters
里面。
resetStoreVM(this, state)
初始化
store._vm
响应式的并且注册
_wrappedGetters
作为computed
的属性
1 | 复制代码plugins.forEach(plugin => plugin(this)) |
插件:把实例对象 store
传给插件函数,执行所有插件。
1 | 复制代码const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools |
初始化 vue-devtool
开发工具。
参数 devtools
传递了取 devtools
否则取Vue.config.devtools
配置。
初读这个构造函数的全部源代码。会发现有三个地方需要重点看。分别是:
1 | 复制代码this._modules = new ModuleCollection(options) |
阅读时可以断点调试,赋值语句this._modules = new ModuleCollection(options)
,如果暂时不想看,可以直接看返回结果。installModule
,resetStoreVM
函数则可以断点调试。
class ModuleCollection
收集模块,构造模块树结构。
注册根模块 参数
rawRootModule
也就是Vuex.Store
的options
参数未加工过的模块(用户自定义的),根模块
1 | 复制代码export default class ModuleCollection { |
1 | 复制代码/** |
class Module
1 | 复制代码// Base data struct for store's module, package with some attribute and method |
经过一系列的注册后,最后this._modules = new ModuleCollection(options)
this._modules
的值是这样的。
笔者画了一张图表示:
installModule 函数
1 | 复制代码function installModule (store, rootState, path, module, hot) { |
注册 state
1 | 复制代码// set state |
最后得到的是类似这样的结构且是响应式的数据 实例 Store.state 比如:
1 | 复制代码{ |
1 | 复制代码const local = module.context = makeLocalContext(store, namespace, path) |
module.context
这个赋值主要是给helpers
中mapState
、mapGetters
、mapMutations
、mapActions
四个辅助函数使用的。生成本地的dispatch、commit、getters和state。
主要作用就是抹平差异化,不需要用户再传模块参数。
遍历注册 mutation
1 | 复制代码module.forEachMutation((mutation, key) => { |
1 | 复制代码/** |
遍历注册 action
1 | 复制代码module.forEachAction((action, key) => { |
1 | 复制代码/** |
遍历注册 getter
1 | 复制代码module.forEachGetter((getter, key) => { |
1 | 复制代码/** |
遍历注册 子模块
1 | 复制代码module.forEachChild((child, key) => { |
resetStoreVM 函数
resetStoreVM(this, state, hot)
初始化
store._vm
响应式的并且注册
_wrappedGetters
作为computed
的属性
1 | 复制代码function resetStoreVM (store, state, hot) { |
到此,构造函数源代码看完了,接下来看 Vuex.Store
的 一些 API
实现。
Vuex.Store 实例方法
commit
提交 mutation
。
1 | 复制代码commit (_type, _payload, _options) { |
commit
支持多种方式。比如:
1 | 复制代码store.commit('increment', { |
unifyObjectStyle
函数将参数统一,返回 { type, payload, options }
。
dispatch
分发 action
。
1 | 复制代码dispatch (_type, _payload) { |
replaceState
替换 store
的根状态,仅用状态合并或时光旅行调试。
1 | 复制代码replaceState (state) { |
watch
响应式地侦听 fn 的返回值,当值改变时调用回调函数。
1 | 复制代码/** |
subscribe
订阅 store
的 mutation
。
1 | 复制代码subscribe (fn) { |
1 | 复制代码// 收集订阅者 |
subscribeAction
订阅 store
的 action
。
1 | 复制代码subscribeAction (fn) { |
registerModule
注册一个动态模块。
1 | 复制代码/** |
unregisterModule
卸载一个动态模块。
1 | 复制代码/** |
hotUpdate
热替换新的 action
和 mutation
。
1 | 复制代码// 热加载 |
组件绑定的辅助函数
文件路径:vuex/src/helpers.js
mapState
为组件创建计算属性以返回 Vuex store
中的状态。
1 | 复制代码export const mapState = normalizeNamespace((namespace, states) => { |
normalizeNamespace 标准化统一命名空间
1 | 复制代码function normalizeNamespace (fn) { |
1 | 复制代码// 校验是否是map 是数组或者是对象。 |
1 | 复制代码/** |
module.context
这个赋值主要是给 helpers
中 mapState
、mapGetters
、mapMutations
、mapActions
四个辅助函数使用的。
1 | 复制代码// 在构造函数中 installModule 中 |
这里就是抹平差异,不用用户传递命名空间,获取到对应的 commit、dispatch、state、和 getters
getModuleByNamespace
1 | 复制代码function getModuleByNamespace (store, helper, namespace) { |
看完这些,最后举个例子:vuex/examples/shopping-cart/components/ShoppingCart.vue
1 | 复制代码computed: { |
没有命名空间的情况下,最终会转换成这样
1 | 复制代码computed: { |
假设有命名空间’ruochuan’,
1 | 复制代码computed: { |
则会转换成:
1 | 复制代码computed: { |
mapGetters
为组件创建计算属性以返回 getter
的返回值。
1 | 复制代码export const mapGetters = normalizeNamespace((namespace, getters) => { |
举例:
1 | 复制代码computed: { |
最终转换成:
1 | 复制代码computed: { |
mapActions
创建组件方法分发 action
。
1 | 复制代码export const mapActions = normalizeNamespace((namespace, actions) => { |
mapMutations
创建组件方法提交 mutation
。
mapMutations 和 mapActions 类似,只是 dispatch 换成了 commit。
1 | 复制代码let commit = this.$store.commit |
mapMutations
、mapActions
举例:
1 | 复制代码{ |
最终转换成
1 | 复制代码{ |
由此可见:这些辅助函数极大地方便了开发者。
createNamespacedHelpers
创建基于命名空间的组件绑定辅助函数。
1 | 复制代码export const createNamespacedHelpers = (namespace) => ({ |
就是把这些辅助函数放在一个对象中。
插件
插件部分文件路径是:
vuex/src/plugins/devtool
vuex/src/plugins/logger
文章比较长了,这部分就不再叙述。具体可以看笔者的仓库 vuex-analysis vuex/src/plugins/
的源码注释。
总结
文章比较详细的介绍了vuex
、vue
源码调试方法和 Vuex
原理。并且详细介绍了 Vuex.use
安装和 new Vuex.Store
初始化、Vuex.Store
的全部API
(如dispatch
、commit
等)的实现和辅助函数 mapState
、mapGetters
、 mapActions
、mapMutations
createNamespacedHelpers
。
文章注释,在vuex-analysis源码仓库里基本都有注释分析,求个star
。再次强烈建议要克隆代码下来。
1 | 复制代码git clone https://github.com/lxchuan12/vuex-analysis.git |
先把 Store
实例打印出来,看具体结构,再结合实例断点调试,事半功倍。
Vuex
源码相对不多,打包后一千多行,非常值得学习,也比较容易看完。
如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。
推荐阅读
美团明裔:Vuex框架原理与源码分析这篇文章强烈推荐,流程图画的很好
知乎黄轶:Vuex 2.0 源码分析这篇文章也强烈推荐,讲述的比较全面
小虫巨蟹:Vuex 源码解析(如何阅读源代码实践篇)这篇文章也强烈推荐,主要讲如何阅读源代码
小生方勤:【前端词典】从源码解读 Vuex 注入 Vue 生命周期的过程
笔者另一个系列
关于
作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客-若川,使用vuepress
重构了,阅读体验可能更好些
掘金专栏,欢迎关注~
segmentfault
前端视野专栏,欢迎关注~
知乎前端视野专栏,欢迎关注~
github blog,相关源码和资源都放在这里,求个star
^_^~
欢迎加微信交流 微信公众号
可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12
,注明来源,拉您进【前端视野交流群】。
若川视野
本文使用 mdnice 排版
本文转载自: 掘金