vuex 源码阅读

本文对应版本 b58d3d6a6426e901175a04bf6dcf206561cc82f5

获得代码

1
2
3
4
git init
git remote add origin https://github.com/vuejs/vuex.git
git fetch origin
git checkout b58d3d6a6426e901175a04bf6dcf206561cc82f5

总览

是个啥,状态管理工具

先了解如何用

前置知识

https://cn.vuejs.org/v2/guide/mixins.html

https://cn.vuejs.org/v2/guide/plugins.html

目录

1
2
3
4
5
6
7
8
9
10
11
12
13
./
├── helpers.js 简化 一些映射代码的编写,建议最后看【以下文档写的是按照我的阅读顺序写的】
├── index.esm.js 为了注入Vue
├── index.js 为了注入Vue
├── mixin.js 为了注入Vue
├── module
│   ├── module-collection.js 模块化的树状结构,主要对原来json配置的改为内部的实现结构
│   └── module.js 模块化的实现 单个模块
├── plugins
│   ├── devtool.js 辅助
│   └── logger.js 辅助
├── store.js 核心实现,实现commit,dispatch 等,对module分析结果 进行再解构,让访问简洁,同时做一些 访问保护检测
└── util.js 单纯工具与vuex 关系性极小

依赖(表面的)

文件 import 依赖
mixin
util
helpers
store.js 依赖见下方
index.js/index.esm.js store.js & helpers.js
plugins/devtool
plugins/logger util:deepCopy
module/module util:forEachValue
module/module-collection module:Module , util:{ assert, forEachValue }

之所以说是表面的,实际上比如logger是有调用store里面的方法的,比如subscribe

代码阅读

index/index.esm.js

主要是方法导出

1
2
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

mixin.js

主要是按照vue的mixin规则,根绝vue版本,向vue里注入 vuex,也就是$store

规则是,有this.$options.store则 this.$store= $options.store/store() 根据是不是函数

没有的话,用this.$options.parent.$store

1
2
3
4
5
6
7
8
9
10
11
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
}
}

util.js

1
2
3
4
5
6
7
8
9
10
11
export function find (list, f) //list中f函数返回true的第一个值

export function deepCopy (obj, cache = []) // 带有处理指针循环结构的 deepCopy

export function forEachValue (obj, fn) // 遍历obj的所有k-v

export function isObject (obj)

export function isPromise (val)

export function assert (condition, msg)

plugins/

devtool

目测是一些调试相关的钩子挂载,TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const target = typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
if (!devtoolHook) return

store._devtoolHook = devtoolHook

devtoolHook.emit('vuex:init', store)

devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})

store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
})
}

logger

嗯 第一行 // Credits: borrowed code from fcomb/redux-logger

借来的代码,依赖上 只用了util的deepCopy

基本上是输出 state状况用于调试的

module/

module

1
2
3
4
5
6
7
8
Module:
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}

child 操作:对this._children的增删

update(rawModule):对namespaced,actions,mutations,getters 有则覆盖的更新

1
2
3
4
5
6
7
8
9
10
11
12
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}

除此以外就是 在该class上 foreach再封装了

1
2
3
4
forEachChild (fn) 
forEachGetter (fn)
forEachAction (fn)
forEachMutation (fn)

module-collection

export:

1
2
3
4
5
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}

这边一个runtime有啥用,这里传的false,下面默不传是true,在Module里只是存一下,TODO

function makeAssertionMessage // 生成 assert错误message

使用

1
2
3
4
5
6
7
8
9
10
11
const functionAssert = {

const objectAssert = {

const assertTypes = {
getters: functionAssert,
mutations: functionAssert,
actions: objectAssert
}

function assertRawModule (path, rawModule) {

来检测是否是个合法的RawModule

这里实现可以看到,rawModule也可以不含getters/mutations/actions,如果含有则需要分别是function/function/object(function / obj.handler == function)

然后发现一个有点意思的东西,之前没看过源码还不知道, process.env.NODE_ENV !== 'production'才会assert

也就是开发环境才会assert,产品环境去掉

get:按照 从this.root开始,按照上面设计的Module Class的层级访问, this.root._children[path[0]]._children[path[1]]...

解释register

1
register (path, rawModule, runtime = true) {

靠外部传来的rawModule,作用是把 rawModule中描述的层级关系的modules,递归的解析成为内部实现的Module,这里的path只是配合内部实现的get,方便查找树状上的Module,是临时生成的一个传递变量

这里也可以看到,实现过程中重名的话是后者覆盖前者.

unregister (path) {//通过path定位 也就是上面get的办法,然后删除,需要满足 路径上的runtime都为true TODO

getNamespace (path) {

看起来像是 path.join('/')实际上,是只会join有namespaced的每一层

update (rawRootModule) / function update (path, targetModule, newModule) {

这两个update的作用就是在已经建立Module树上,进行更新,其中如果遇到树的结构不同则忽略掉,更新的过程不会变更树结构,只会对namespaced、actions、mutations、getters进行替换(更新)

helpers

export:

1
2
3
4
5
6
export const createNamespacedHelpers = (namespace) => ({
mapState: mapState.bind(null, namespace),
mapGetters: mapGetters.bind(null, namespace),
mapMutations: mapMutations.bind(null, namespace),
mapActions: mapActions.bind(null, namespace)
})

function normalizeMap (map) 把array,obj转化成[{'key':,'val':},...]

function normalizeNamespace (fn) { // 函数参数预处理(namespace,map) -> fn(namespace,map)

function getModuleByNamespace (store, helper, namespace) { return store._modulesNamespaceMap[namespace]

上面封了一层以后,每个map××× 被export出的都是单参数了

看了具体实现

1
2
3
4
mapState: .vuex=true {支持devtools}
mapMutations:
mapActions:
mapGetters: .vuex=true {支持devtools}

除了getter,其它都是有namespace用namespace所指的,没有就用全局的this.$store里的

getter 特殊在全是在this.$store.getters[]里,如果有namespace则是namespace+val构成新的val

Store

最后,最大的一个,505行

1
2
3
4
import applyMixin from './mixin'
import devtoolPlugin from './plugins/devtool'
import ModuleCollection from './module/module-collection'
import { forEachValue, isObject, isPromise, assert } from './util'

export:

install && Class Store

先看install,也就是调用applyMixin(Vue)进行注入

Store上这里分析源码,省略一些错误判断的解释了,只要说说逻辑

Store:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
constructor (options = {}) {
const {
plugins = [],
strict = false
} = options

// store internal state
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options) // 这里调用前面实现的 递归解析options中的modules
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue() // TODO 这个是什么用

// bind commit and dispatch to self // 这一段没有看懂,是为了兼容哪个版本的js吗,这两个本身已经实现了在下面了啊 怎么还要再绑定一次 TODO
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 //控制是否启用StrictMode store._vm.$watch(function () { return this._data.$$state }, 严格要求禁止在mutation handler以外的地方修改_data.$$state

实现原理是 利用vue进行watch

在允许的函数修改的时候修改`_committing 为 true` 环绕 见`_withCommit`

当watch到修改时,判断是否`_committing=true`这样就可以判断是否被外部修改

this.strict = strict

const state = this._modules.root.state // 获取的是 生成的module的根部的state

// 递归初始化 root module 以及所有子modules.
// 并且收集所有module getters mutation等 到store._下面 比如`_wrappedGetters`
installModule(this, state, [], this._modules.root)

// 初始化 store._vm , 作为回调的使用
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

// 使用插件
plugins.forEach(plugin => plugin(this))

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}

function unifyObjectStyle (type, payload, options) { //感觉是特殊处理type的情况 返回{type,payload,options}

makeLocalContext(store,namespace,path)

如果没有namespace 就用root上的

如果有则建立 本地化 的 dispatch commit getters 和 state

建立的实现过程 一个是调用unifyObjectStyle处理type的特殊情况,另一个就是给type 加上namespace

getters和state,使用defineProperties写到local上,注释说是因为它们会被vm更新,应该lazily的获取

其中getters的makeLocalGetters的原理是 namespace对比,和从store.getters[type]中读取

makeLocalContext的结果会写入到module.context

installModule

如果当前路径有namespace, 那把它丢到store._modulesNamespaceMap[namespace] 和之前的获取对应

有点没看懂 set state,按理说 在 new ModuleCollection(options)就应该已经 在 new Module的部分设置了state,这里是为了触发Vue.set()?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
// 简单的讲 就是把 所有mutations压入 `store._mutations[namespace+mutationname],之所以是压入,是因为 可能出现多个相同的 namespace+mutationname
})

module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
// 入口参数和上面还是类似,这个是塞入 store._actions
// 但是看了源码才知道,action的交付的第二个参数
// {
// dispatch: local.dispatch,
// commit: local.commit,
// getters: local.getters,
// state: local.state,
// rootGetters: store.getters,
// rootState: store.state }

})

module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
// 入口参数和上面还是类似,这个是塞入 store._wrappedGetters
})

并对所有子module installModule()

整个install就是把 构建出的module 拆出所有的 操作函数

resetStoreVM

store._vm = new Vue({data:{$$state:state},computed}) 其中 computed是 上面 提取出的_wrappedGetters改动的,改动出的computed将 有和getters相同的keys,区别是 它会从store._vm[key]的方式去读数据

在这一步设置_vm时,临时设置Vue.config.silent

如果原来有._vm则调用销毁

vuex API

至此 内部的基本结构就结束了,来看看调用api对应的实现

先看 官网的图 VUEX

store.state.xxx

1
2
3
get state () {
return this._vm._data.$$state
}

commit(type,payload,options)

这里实现的过程,通过解构出的this._mutations[type]得到type对应的mutations 的函数句柄,

然后调用有withcommit包围的,进行 函数 执行

所有执行以后,对所有_subscribers 通知(mutation,new state) , 简单的订阅模式

1
2
3
grep -r "subscribe" *
plugins/logger.js: store.subscribe((mutation, state) => {
plugins/devtool.js: store.subscribe((mutation, state) => {

dispatch (type, payload)

同理 通过解构出的this._actions[type]获得actions

区别是 action的订阅者 可以指定 before 和 after,分别会在action发生前后调用,

subscribeAction (fn) 如果fn是函数则 默认变为订阅action发生前 ,否则fn应该是类似{'before':()=>{},'after':()=>{}}的格式

关于订阅的调用,目前只在测试里看到有,其它部分没有调用

除了环绕的订阅以外,就是执行对应的action,entry,如果是多个 调用Promise.all(),如果是一个直接entry0

剩余的

剩余的 hotUpdate,replaceState等 应该属于调试时使用的不属于核心功能了,watch 基本是靠的Vue的$watch

总结

  1. 整个源码阅读完后,才发现原来根本没有用到module这一部分,甚至都不知道,看来是没仔细读文档,原来这一块已经实现好了。
  2. 从Module部分可以看到,实现rawModule相关 过程是 加了一层套把rawModule套了一层,保护了树的结构,做到期望内的更新
  3. 潜在bug? 从外部传入rawModule 的第一次构建是 _rawModule=rawModule,而在后面update的过程中是修改_rawModule的字段,可能导致 期望外的修改? 不过根据代码,这一部分只会发生在hotUpdate

个人其它收获

  1. 实际实现的代码 除了实现想法外还有很多细节,这些细节感觉也蛮庞大?的
  2. 这样path的写法? 省一些指针?感觉每次get代价会大一些
  3. reduce 的用法(http://www.runoob.com/jsref/jsref-reduce.html)
  4. bind的用法(https://blog.csdn.net/kongjunchao159/article/details/59113129)
  5. es6 destructuring (https://www.deadcoderising.com/2017-03-28-es6-destructuring-an-elegant-way-of-extracting-data-from-arrays-and-objects-in-javascript/)
  6. get,defineProperties , 都是在调用时才计算,区别是get在原型上,而defineProperties 是在实例上(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Smart_self-overwriting_lazy_getters)
  7. 同时 其中实现,有些注释错误,有些函数的默认参数没有设置 =false,不过个人对js不太熟,要是c++的强迫症话 感觉还是要加的
  8. getters ,函数->计算属性
  9. 普通对象变化不会影响刷新,需要有getter和setter的,简单的方法就是用 new Vue({data:{xxx}});来包裹会自动加上 getter和setter
  10. 发布订阅
  11. strict通过 包裹限制 只有mutation改动数据, 但建议仅在开发时使用,在发布时为了性能不使用strict

其他:

感觉要去看看Promise的源码 以及 Vue的源码了

TODO

Module 里的runtime有啥用

重复注册state、mutation等等? 有文章说是 因为module的原因,

modules使用

modules{
模块名:{
state{
x:1,
y:1
}
},
模块名2:{

}
}

模块内的mutations,getters,actions 不同层级都会被调用!? 都会被合并到根上

所以要做的是方法映射出来,但是 把每个方法的所属模块的state绑定过去

但是 只有mutations和actions是数组,getters是由 defineProperties来的所以只会有一个

TODO

namespace:true 可以让模块化,不让actions等绑定在global上 通过module/module路径访问,实现就是 根下的 map[ 拼接的module namespace路径实现的]

registerModule 动态注册模块

1
2
3
4
5
6
registerModule (path, rawModule, options = {}) {

this._modules.register(path, rawModule)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)

store.subscribe() vuex中间件?? ,也是发布订阅模式

注册的是 和 函数指针放入数组,返回移出数组的函数,这个和vue-router里的registerHooks一样

commit后

1
2
3
4
5
6
7
8
commit里
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 提交后 调用 所有订阅
this._subscribers.forEach(sub => sub(mutation, this.state))