vuex源码简析
前言
基于 vuex 3.1.2 按如下流程进行分析:
Vue.use(Vuex);
const store = new Vuex.Store({
actions,
getters,
state,
mutations,
modules
// ...
});
new Vue({store});
Vue.use(Vuex)
Vue.use() 会执行插件的 install 方法,并把插件放入缓存数组中。
而 Vuex 的 install 方法很简单,保证只执行一次,以及使用 applyMixin 初始化。
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 方法定义在 vuex/src/mixin.js
,vuex 还兼容了 vue 1 的版本,这里只关注 vue 2 的处理。
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 混入一个 beforeCreate 方法
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 这里是 vue 1 的兼容处理
}
// 这里的逻辑就是将 options.store 保存在 vue 组件的 this.$store 中,
// options.store 就是 new Vue({...}) 时传入的 store,
// 也就是 new Vuex.Store({...}) 生成的实例。
function vuexInit () {
const options = this.$options
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
}
}
}
new Vuex.Store({...})
先看 Store 的构造函数:
constructor (options = {}) {
const { plugins = [], strict = false } = options
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// 构建 modules
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
// store.watch
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// 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)
}
// strict mode
this.strict = strict
// 安装模块
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
// 实现状态的响应式
resetStoreVM(this, state)
// apply plugins
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
this._modules
vuex 为了让结构清晰,允许我们将 store
分割成模块,每个模块拥有自己的 state
、mutation
、action
、getter
,而且模块自身也可以拥有子模块。
const moduleA = {...}
const moduleB = {...}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
而模块在 vuex 的构造函数中通过new ModuleCollection(options)
生成,依然只看构造函数:
// vuex/src/module/module-collection.js
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
register (path, rawModule, runtime = true) {
// 生成一个 module
const newModule = new Module(rawModule, runtime)
// new Vuex.Store() 生成的是根模块
if (path.length === 0) {
// 根模块
this.root = newModule
} else {
// 生成父子关系
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)
})
}
}
}
register
接收 3 个参数,其中 path
表示路径,即模块树的路径,rawModule
表示传入的模块的配置,runtime
表示是否是一个运行时创建的模块。
register
首先 new Module() 生成一个模块。
// vuex/src/module/module.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 子模块
this._children = Object.create(null)
// module 原始配置
this._rawModule = rawModule
const rawState = rawModule.state
// state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
实例化一个 module 后,判断当前的 path 的长度,如果为 0,就是是一个根模块,所以把 newModule 赋值给 this.root,而 new Vuex.Store() 生成的就是根模块。
如果不为 0,就建立父子模块的关系:
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
首先根据路径获取父模块,然后再调用父模块的 addChild 方法将子模块加入到 this._children 中,以此建立父子关系。
register 最后会检测是否有嵌套的模块,然后进行注册嵌套的模块:
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
遍历当前模块定义中的所有 modules,根据 key 作为 path,递归调用 register 方法进行注册。
installModule
注册完模块就会开始安装模块:
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
installModule 方法支持 5 个参数: store,state,path(模块路径),module(根模块),hot(是否热更新)。
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的,这样使得多个模块能够对同一 mutation 或 action 作出响应。但是如果有同名的 mutation 被提交,会触发所有同名的 mutation。
因此 vuex 提供了 namespaced: true
让模块成为带命名空间的模块,当模块被注册后,它的所有 action、mutation 和 getter 都会自动根据模块注册的路径调整命名。例如:
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
})
回到 installModule 方法本身:
function installModule (store, rootState, path, module, hot) {
// 是否为根模块
const isRoot = !path.length
// 获取命名空间,
// 比如说 { a: moduleA } => 'a/',
// root 没有 namespace
const namespace = store._modules.getNamespace(path)
// 将有 namespace 的 module 缓存起来
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
if (!isRoot && !hot) {
// 给 store.state 添加属性,
// 假如有 modules: { a: moduleA },
// 则 state: { a: moduleA.state }
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// local 上下文,
// 本质上是重写 dispatch 和 commit 函数,
// 举个例子,modules: { a: moduleA },
// moduleA 中有名为 increment 的 mutation,
// 通过 makeLocalContext 函数,会将 increment 变成
// a/increment,这样就可以在 moduleA 中找到定义的函数
const local = module.context = makeLocalContext(store, namespace, path)
// 下面这几个遍历循环函数,
// 都是在注册模块中的 mutation、action 等等,
// moduleA 中有名为 increment 的 mutation,
// 那么会将 namespace + key 拼接起来,
// 变为 'a/increment'
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
// 会用 this._mutations 将每个 mutation 存储起来,
// 因为允许同一 namespacedType 对应多个方法,
// 所以同一 namespacedType 是用数组存储的,
// store._mutations[type] = []
registerMutation(store, namespacedType, mutation, local)
})
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
// 和上面一样,用 this._actions 将 action 存储起来
registerAction(store, type, handler, local)
})
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
// 会用 this._wrappedGetters 存储起来
// getters 有一点不一样,
// 这里保存的是一个返回 getters 的函数,
// 而且同一 namespacedType 只能定义一个。
registerGetter(store, namespacedType, getter, local)
})
// 递归安装子模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
makeLocalContext
function makeLocalContext (store, namespace, path) {
// 判断是否有 namespace
const noNamespace = namespace === ''
const local = {
// 重写 dispatch
// 为什么要重写,
// 举个例子 modules: { a: moduleA }
// 在 moduleA 的 action 中使用 dispatch 时,
// 并不会传入完整的 path,
// 只有在 vue 实例里调用 store.dispatch 才会传入完整路径
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
}
return store.dispatch(type, payload)
},
// 重写 commit 方法
// 同上
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
}
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
// local.getters 本质上是通过匹配 namespace,
// 从 store.getters[type] 中获取
: () => makeLocalGetters(store, namespace)
},
state: {
// local.state 本质上是通过解析 path,
// 从 store.state 中获取
get: () => getNestedState(store.state, path)
}
})
return local
}
resetStoreVM(this, state)
初始化 store._vm,利用 Vue 将 store.state 进行响应式处理,并且将 getters 当作 Vue 的计算属性来进行处理:
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历 store._wrappedGetters 属性
forEachValue(wrappedGetters, (fn, key) => {
// 这里的 partial 其实就是创建一个闭包环境,
// 保存 fn, store 两个变量,并赋值给 computed
computed[key] = partial(fn, store)
// 重写 get 方法,
// store.getters.key 其实是访问了 store._vm[key],
// 也就是去访问 computed 中的属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// 实例化一个 Vue 实例 store._vm,
// 用它来保存 state,computed(getters),
// 也就是利用 Vue 的进行响应式处理
const silent = Vue.config.silent
Vue.config.silent = true
// 访问 store.state 时,
// 其实是访问了 store._vm._data.$$state
store._vm = new Vue({
data: {
$$state: state
},
// 这里其实就是上面的 getters
computed
})
Vue.config.silent = silent
// 开启 strict mode,
// 只能通过 commit 的方式改变 state
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
if (hot) {
// 热重载
store._withCommit(() => {
oldVm._data.$$state = null
})
}
// 这里其实是动态注册模块,
// 将新的模块内容加入后生成了新的 store._vm,
// 然后将旧的销毁掉
Vue.nextTick(() => oldVm.$destroy())
}
}
partial
export function partial (fn, arg) {
return function () {
return fn(arg)
}
}
常用 api
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) {
// 没有 mutation 会报错并退出
return
}
this._withCommit(() => {
// 允许同一 type 下,有多个方法,
// 所以循环数组执行 mutation,
// 实际上执行的就是安装模块时注册的 mutation
// handler.call(store, local.state, payload)
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 触发订阅了 mutation 的所有函数
this._subscribers
.slice()
.forEach(sub => sub(mutation, this.state))
}
dispatch
dispatch (_type, _payload) {
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 从 Store._actions 获取
const entry = this._actions[type]
if (!entry) {
// 找不到会报错
return
}
// 在 action 执行之前,
// 触发监听了 action 的函数
try {
this._actionSubscribers
.slice()
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
console.error(e)
}
// action 用 Promise.all 异步执行,
// 实际上执行的就是安装模块时注册的 action
/*
handler.call(store, {dispatch, commit, getters, state, rootGetters, rootState}, payload, cb)
*/
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return result.then(res => {
// 在 action 执行之后,
// 触发监听了 action 的函数
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
console.error(e)
}
return res
})
}
watch
watch (getter, cb, options) {
// new Vuex.Store() 时创建 _watcherVM,
// this._watcherVM = new Vue(),
// 本质就是调用 vue 的 api
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
registerModule
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
// 注册模块
this._modules.register(path, rawModule)
// 安装模块
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// 重新生成 vue 实例挂载到 store 上,
// 然后销毁旧的
resetStoreVM(this, this.state)
}
备注
希望疫情尽快过去吧。——2020/02/08 元宵
vuex源码简析的更多相关文章
- SpringMVC学习(一)——概念、流程图、源码简析
学习资料:开涛的<跟我学SpringMVC.pdf> 众所周知,springMVC是比较常用的web框架,通常整合spring使用.这里抛开spring,单纯的对springMVC做一下总 ...
- Flink源码阅读(一)——Flink on Yarn的Per-job模式源码简析
一.前言 个人感觉学习Flink其实最不应该错过的博文是Flink社区的博文系列,里面的文章是不会让人失望的.强烈安利:https://ververica.cn/developers-resource ...
- django-jwt token校验源码简析
一. jwt token校验源码简析 1.1 前言 之前使用jwt签发了token,里面的头部包含了加密的方式.是否有签名等,而载荷中包含用户名.用户主键.过期时间等信息,最后的签名还使用了摘要算法进 ...
- 0002 - Spring MVC 拦截器源码简析:拦截器加载与执行
1.概述 Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理.例如通过拦截器可以进行权限验证.记录请求信息的日 ...
- OpenStack之Glance源码简析
Glance简介 OpenStack镜像服务器是一套虚拟机镜像发现.注册.检索. glance架构图: Glance源码结构: glance/api:主要负责接收响应镜像管理命令的Restful请求, ...
- AFNetworking源码简析
AFNetworking基本是苹果开发中网络请求库的标配,它是一个轻量级的网络库,专门针对iOS和OS X的网络应用设计,具有模块化的架构和丰富的APIs接口,功能强大并且使用简单,深受苹果应用开发人 ...
- ElementUI 源码简析——源码结构篇
ElementUI 作为当前运用的最广的 Vue PC 端组件库,很多 Vue 组件库的架构都是参照 ElementUI 做的.作为一个有梦想的前端(咸鱼),当然需要好好学习一番这套比较成熟的架构. ...
- DRF之APIView源码简析
一. 安装djangorestframework 安装的方式有以下三种,注意,模块就叫djangorestframework. 方式一:pip3 install djangorestframework ...
- spring ioc源码简析
ClassPathXmlApplicationContext 首先我们先从平时启动spring常用的ClassPathXmlApplicationContext开始解析 ApplicationCont ...
随机推荐
- Java线程的生命周期与状态流转
上图是一个线程的生命周期状态流转图,很清楚的描绘了一个线程从创建到终止的过程. 这些状态的枚举值都定义在java.lang.Thread.State下 NEW:毫无疑问表示的是刚创建的线程,还没有开始 ...
- flask修改数据库字段的类型和长度
flask修改数据库字段的类型和长度 在将models中的字段的db.String(256)修改为db.String(1024)后,执行migrate和upgrade操作后,发现数据库并没有更新, ...
- $CF949D\ Curfew$ 二分/贪心
正解:二分/贪心 解题报告: 传送门$QwQ$ 首先这里是二分还是蛮显然的?考虑二分那个最大值,然后先保证一个老师是合法的再看另一个老师那里是否合法就成$QwQ$. 发现不太会搞这个合不合法的所以咕了 ...
- Qt5学习(1)
1. In Qt, if you want to apply styles to the main window itself, you must apply it to its central ...
- git版本管理工具(二)
1.查看历史版本 ·git log ·git reflog 2.版本回退 ·git reset --hard HEAD^(HEAD代表当前版本) ·HEAD^代表回退到上一个版本 以此类推 ·HEAD ...
- linux下挂载硬盘出错的解决方法
我的电脑是 Uuntu16.04 + win10 双系统,今天在Ubuntu中打开D盘时报错 Error mounting /dev/sda5 原因是D盘的格式是ntfs,在linux中会出现不识别的 ...
- 79.纯 CSS 创作单元素麦当劳金拱门 Logo(自创)
效果地址:https://scrimba.com/c/cN3P6nfr 原理:两个椭圆,颜色部分为边框,下一半被伪元素覆盖. 感想:看了一眼大神的,代码比我的还少,得研究研究去. HTML code: ...
- Spring学习记录4——Spring对DAO的支持
Spring对DAO的支持 随着持久化技术的持续发展,Spring对多个持久化技术提供了集成支持,包括Hibernate.MyBatis.JPA.JDO:此外,还提供了一个简化JDBC API操作的S ...
- 【LC_Overview1_5】---学会总结回顾
刷LeetCode题目一周,主要采用C++和Python编程手段,截至目前做了5道简单的leetcode题目,做下阶段性的小结: 小结主要通过手撕代码,复习加回顾,尽量避免自己眼高手低的情况发生,对于 ...
- MyEclipse导出war包丢失文件问题解决
这两天忙于一项目的上线,现总结一下遇到的一个奇怪问题的解决方案. 公司用的是Windows系统的服务器,所以省去了很多linux的繁琐命令.部署工作简单了很多.一切准备结束,放上War包启动服务器后, ...