使用vue已经有半年有余, 在各种正式非正式项目中用过, 开始专注于业务比较多, 用到现在也遇见不少因为理解不深导致的问题. 有问题就有找原因的勇气, 所以带着问题搞一波.

带着问题看源码

所以来整理了一下使用过程中不注意或者不规范, 或者简化写法的奇技淫巧, 会结合文档的说明和实际的问题来看看源码, 问题:

  • module在vuex里实际的数据结构
  • namespaced在vuex里实际的数据结构
  • mapState, mapActions等helper的正确用法(配合module/namespaced), 或者是否存在更多骚用法
  • mutation中赋值/触发state变化原理

源码分析

看的源码版本为vuex2.3.1

我们使用vuex可能是类似:

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
import Vue from 'vue'
import Vuex from 'vuex'
import plugins from './plugins'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todo: ["todo1", "todo2"]
},
mutations: {
mutationName(state, payload) {
state.xxx = payload.xxx
}
},
actions: {
actionName({ commit, dispatch }, payload) {
commit(mutationName, payload)
}
},
modules: {
catagories: {
state: {},
mutations: {}
}
},
plugins
})

使用vuex的方法为使用Vue.use来installvuex, 并new一个Store实例, 我们来看一下vuex核心对象.

Store对象分析

line6: 本地vue变量, 在install时会被赋值, 之后会通过vue是否为undefined来判断是否install

Store对象构建

line10~14: 判断vuex是否被正确使用
line16~26: 获取options, state可以和vue的component的data一样为函数return一个对象, 会在这段代码中被parse
line28~36: store对象内部变量初始化
line39~46: 绑定commit和dispatch方法到自身
line54: 装载动作
line58: 装载响应动作
line61: 调用插件

store内部变量初始化

1
this._committing = false

是否合法更新state的标识, 对象有方法_withCommit是唯一可以改动_committing的方法, 只有对象内部使用_withCommit更新状态才是合法的, 在strict模式下非法更新state会抛出异常.

1
this._modules = new ModuleCollection(options)

modules的cache, 直接把store的参数全部扔给了ModuleCollection新建一个modules对象.

点击跳转ModuleCollection对象来看分析.

1
this._modulesNamespaceMap = Object.create(null)
1
this._subscribers = []
1
this._watcherVM = new Vue()

其余的变量是新建了空的变量, 之后会在install模块的时候赋值.

绑定dispatch和commit方法

line39~46, 对dispatch和commit方法进行绑定, 使dispatch方法可以调用在Store对象上注册过的._actions

._mutations的方法.

dispatch方法在line108, 先兼容了参数的写法, 取到参数, 然后判断Store对象的_.actions属性是否注册过, 如果注册过多个, 将会依次调用. 也就是如果type重复了也是会调用多次的, 这个地方如果出错debug会非常困难. 暂时没有理解vuex此处设计的意图.

commit方法稍微多一点, 大体思路是一样的, 只是直接执行没有返回值, dispatch会返回执行结果. 另外在line95进行了subscriber的操作, 我们暂且不知道subscriber的作用. 稍后再看.

install模块

首先来看参数:

1
2
3
function installModule (store, rootState, path, module, hot)
// 调用
installModule(this, state, [], this._modules.root)

line255 根据path获得namespace, 做法是读取path的每个模块, 如果namespaced为true则拼接, 例如path为['catagories', 'price', 'detail'], 其中price的namespaced为false, 其余为true, 那么获得的namespace为catagories/detail/.

line258~260 把namespaced为true的module注册到_modulesNamespaceMap.

line271makeLocalContext函数整理了namespace和type的关系. 在之后的三个module.forEachXxx中, 都调用了registerXxx, 最后的参数都是makeLocalContext的返回值. 我们来分析一下makeLocalContext的作用:

被注册到全局的mutation/actiongetter实际的type类似于namespace1/namespace2/type的形式, 而我们在namespaced为true的module中调用的type只是:type. 所以在namespace[true]的action中调用的所有dispatch, commit, getter, state 都会被加上 path.join(“/“) + “/“ 的type来调用到正确的方法.

根据注册的type, 我还得到了一个偏门结论: 可以通过设置type为namespace1/namespace2/type来调用其他namespace的type(待测试), 因为他们是这样被注册的.

install child module

通过比较, install child module的时候是改了第三第四个参数: path => path.concat(key), module => module.getChild(key).

主要区别只是在line264~268, 与ModuleCollection的递归注册子module行为类似, 递归的path参数流程上只是多了一步把当前loop产生的对象挂到父节点上. 做法也是一样的, 把module名字(path)作为key, 套在父级state上. 也就是结构为:

1
2
3
4
5
6
7
8
9
state: {
...currentState,
moduleName: {
...subState
},
module2Name: {
...anotherSubState
}
}

在之前注册Mutation的时候vuex也是通过这个方法来试mutation获得嵌套过的state作为arguments[0]的.

Store对象总结

store对象把传入的options放入了各个变量进行储存, 并提供了commit, dispatch等方法来调用和处理他们:

._modules

这里存放raw的modules, 未经处理的, 以module名字作为key的方式递归子module.

.state

这里也是以module名字作为key的方式递归储存传入的state

entrys

这里的entry指._actions, ._mutations, ._getters. 他们的储存方式并没有递归储存key, 而是平级的, 用/来分割namespace来分辨type, 并在注册时把当前的entry绑定对应的state(通过getNestedState方法).

问题: 如果在不同module注册了相同type的mutation, 会发生什么?

回答: 会依次在自己的state中执行, 不会影响对方state, 但是会造成错误执行. (待测试). 所以应该在大的项目中尽量使用namespaced[true]的方式, 而不是命名的方式.(但是也是可以利用/来串namespace的, 所以自己type命名避免/)

._modulesNamespaceMap

根据namespace为key来存放子module

初始化Store VM

这里会新建一个Vue实例并赋值给Store对象的._vm属性, 把整个vuex的状态放进去. 并判断严格模式, 如果为严格模式会在非法改变状态的时候抛出异常.

这样整个构建动作已经完成了, 那么这个._vm在什么时候用的, 请看下面的章节.

Store对象的属性&方法

line64 state的getter方法, 会获取._vm的vue实例的state. 所以我们在vue代码中this.$store.state.xxx获取到的东西就是这个vue的实例的数据.

line68 当直接set Store的state时报错, 只能通过设置._vm来进行.

剩余的方法的是vuex的进阶用法, 是可以在使用时对vuex状态进行操作的方法, 详见文档

ModuleCollection对象

我们来看下ModuleCollection的构造方法.

register根module

调用了register方法, 把参数的path设为根目录, runtime设为false.

register方法一开始(l30)就判断了除state外的属性的值是否为函数, 若不是则抛出异常.

line33 把module参数(还是初始的options, 就是{state:{...}, mutations:{...}, actions: {...}}这个)和runtime = false 来构建了Module对象(稍后我们看Module对象的构造)

line35 把ModuleCollection的root私有变量设为了刚才使用初始options新建的Module对象.

line42 如果初始options有modules这个属性, 就开始递归注册modules.

递归register子module

上面是register的第一个参数path为空, 也就是root节点的时候的流程, 在最后一部分(line42)根据是否当前注册的module含有modules属性来递归注册, 这部分我们来看一下register的path参数的行为会把数据存成什么结构. 以概览部分的例子的参数为例(modules含有一个key为catagories)来走一遍代码流程. (开始~)

被作为子module传入register方法的参数应该为: path([‘catagories’]), rawModule(state: {},mutations: {}), runtime(false).

注意到的是, 如果catagories有同级module, 被传入的path也是一个元素的数组, 也就是path的意思应该类似于从跟到当前module的层级, 对于兄弟节点是无感的.

这里的runtime尚未明白用途, 可能是在别处调用的. 注册流程应该runtime都为false.

一路看下来, 也是new了一个Module对象, 但是没有走到line35把new出的对象放到root变量里, 而是在line37~38去寻找当前module的父节点并把自己作为child, append到父节点上.

这里又脑补了一下数据结构: path.slice(0, -1)是获取被pop()一下的path, path[path.lengt - 1]是获取当前path的最后一个元素, 也就是当前正在被register的module的key. 所以之前对于path的数据结构判断是正确的.

这里的appendChildgetChild很明显是Module对象的方法了, 我们再继续看Module对象的结构.

Module对象

最后来看Module对象的构造~

接受2个参数, 一个rawModule, 一个runtime, 第一个参数是刚才相对于key为catagories的value, 也就是类似{state: xxx, mutations: xxx, actions: xxx}的options.

Module的构造函数只是把参数拆分, 放入了自己的私有变量, 其中state也接受函数, 并执行函数parse成对象存入私有变量. 其他变量都是原封不动储存的, 所以vuex给他起名为 rawModule 吧. 剩下那些方法都是顾名思义的, 语法上也简单, 没什么好看的.

总结

那么这样Store对象的._modules属性的数据结构已经很清楚了. 类似于(脑内):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
// (ModuleCOllection实例)
root: {
// (Module实例)
_rawModule: {
state: {...}, mutations: {...}, // ...(全是options直接传入)
},
state: {}, // 进行过parse的state, 如果是function会调用并赋值
_children: {
catagories: {
// (Module实例)
},
anotherModule: {
// (Module实例), 递归
}
}
}
}

总结一点, 就是这里贮存的数据都是”raw”的.

helpers

所有的helper都用了两个wrap方法, 先来看下这两个方法的作用.

normalizeNamespace

因为helper是都接受两种传参方式:mapState(namespace, map) / mapState(map) , 如果第一个参数为map时这个函数把namespace设为空字符串 , 并且检查namespace的最后一个字符是不是/, 如果不是的话加上.

normalizeMap

我们map的内容也接受两种语法:

1
2
3
4
[
"state1",
"state2"
]

或者是

1
2
3
4
{
state1: state => state.state1,
state2: state => state.state2
}

这个wrap函数会把两种形式都normalize为含有keyval属性的数组, 便于统一处理. 也就是上面个两个形式会转化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Array like
[{
key: "state1",
val: "state1"
}, {
key: "state2",
val: "state2"
}]
// Object like
[{
key: "state1",
val: state => state.state1
}, {
key: "state2",
val: state => state.state2
}]

mapState

这里做了2个处理:

  • 如果namespace不为空, 把state和getter的环境切换到相对于namespace的环境(就是之前的makeLocalContext的返回值)
  • 如果val为函数则执行, 否则返回state的val为键的属性. 两者的执行环境皆为处理过namespace的local环境.

mapActions

mapAction的val语法只接受字符串的, 所以先把val前借namespace, 变为: namespace/val, 这样能符合在Store里注册的entry名.

然后检查了一下namespace是否被注册过, 也就是防碰撞, 然后把val作为type, 并把剩余参数带着dispatch Store里的action.


参考: