前言


基于 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 分割成模块,每个模块拥有自己的 statemutationactiongetter,而且模块自身也可以拥有子模块。

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源码简析的更多相关文章

  1. SpringMVC学习(一)——概念、流程图、源码简析

    学习资料:开涛的<跟我学SpringMVC.pdf> 众所周知,springMVC是比较常用的web框架,通常整合spring使用.这里抛开spring,单纯的对springMVC做一下总 ...

  2. Flink源码阅读(一)——Flink on Yarn的Per-job模式源码简析

    一.前言 个人感觉学习Flink其实最不应该错过的博文是Flink社区的博文系列,里面的文章是不会让人失望的.强烈安利:https://ververica.cn/developers-resource ...

  3. django-jwt token校验源码简析

    一. jwt token校验源码简析 1.1 前言 之前使用jwt签发了token,里面的头部包含了加密的方式.是否有签名等,而载荷中包含用户名.用户主键.过期时间等信息,最后的签名还使用了摘要算法进 ...

  4. 0002 - Spring MVC 拦截器源码简析:拦截器加载与执行

    1.概述 Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理.例如通过拦截器可以进行权限验证.记录请求信息的日 ...

  5. OpenStack之Glance源码简析

    Glance简介 OpenStack镜像服务器是一套虚拟机镜像发现.注册.检索. glance架构图: Glance源码结构: glance/api:主要负责接收响应镜像管理命令的Restful请求, ...

  6. AFNetworking源码简析

    AFNetworking基本是苹果开发中网络请求库的标配,它是一个轻量级的网络库,专门针对iOS和OS X的网络应用设计,具有模块化的架构和丰富的APIs接口,功能强大并且使用简单,深受苹果应用开发人 ...

  7. ElementUI 源码简析——源码结构篇

    ElementUI 作为当前运用的最广的 Vue PC 端组件库,很多 Vue 组件库的架构都是参照 ElementUI 做的.作为一个有梦想的前端(咸鱼),当然需要好好学习一番这套比较成熟的架构. ...

  8. DRF之APIView源码简析

    一. 安装djangorestframework 安装的方式有以下三种,注意,模块就叫djangorestframework. 方式一:pip3 install djangorestframework ...

  9. spring ioc源码简析

    ClassPathXmlApplicationContext 首先我们先从平时启动spring常用的ClassPathXmlApplicationContext开始解析 ApplicationCont ...

随机推荐

  1. Java线程的生命周期与状态流转

    上图是一个线程的生命周期状态流转图,很清楚的描绘了一个线程从创建到终止的过程. 这些状态的枚举值都定义在java.lang.Thread.State下 NEW:毫无疑问表示的是刚创建的线程,还没有开始 ...

  2. flask修改数据库字段的类型和长度

    flask修改数据库字段的类型和长度 ​ 在将models中的字段的db.String(256)修改为db.String(1024)后,执行migrate和upgrade操作后,发现数据库并没有更新, ...

  3. $CF949D\ Curfew$ 二分/贪心

    正解:二分/贪心 解题报告: 传送门$QwQ$ 首先这里是二分还是蛮显然的?考虑二分那个最大值,然后先保证一个老师是合法的再看另一个老师那里是否合法就成$QwQ$. 发现不太会搞这个合不合法的所以咕了 ...

  4. Qt5学习(1)

    1. In Qt, if you want to apply styles to the main window  itself, you must apply it to  its central ...

  5. git版本管理工具(二)

    1.查看历史版本 ·git log ·git reflog 2.版本回退 ·git reset --hard HEAD^(HEAD代表当前版本) ·HEAD^代表回退到上一个版本 以此类推 ·HEAD ...

  6. linux下挂载硬盘出错的解决方法

    我的电脑是 Uuntu16.04 + win10 双系统,今天在Ubuntu中打开D盘时报错 Error mounting /dev/sda5 原因是D盘的格式是ntfs,在linux中会出现不识别的 ...

  7. 79.纯 CSS 创作单元素麦当劳金拱门 Logo(自创)

    效果地址:https://scrimba.com/c/cN3P6nfr 原理:两个椭圆,颜色部分为边框,下一半被伪元素覆盖. 感想:看了一眼大神的,代码比我的还少,得研究研究去. HTML code: ...

  8. Spring学习记录4——Spring对DAO的支持

    Spring对DAO的支持 随着持久化技术的持续发展,Spring对多个持久化技术提供了集成支持,包括Hibernate.MyBatis.JPA.JDO:此外,还提供了一个简化JDBC API操作的S ...

  9. 【LC_Overview1_5】---学会总结回顾

    刷LeetCode题目一周,主要采用C++和Python编程手段,截至目前做了5道简单的leetcode题目,做下阶段性的小结: 小结主要通过手撕代码,复习加回顾,尽量避免自己眼高手低的情况发生,对于 ...

  10. MyEclipse导出war包丢失文件问题解决

    这两天忙于一项目的上线,现总结一下遇到的一个奇怪问题的解决方案. 公司用的是Windows系统的服务器,所以省去了很多linux的繁琐命令.部署工作简单了很多.一切准备结束,放上War包启动服务器后, ...