这是对vuex3.x版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章只讲解vuex中的state,这也是vuex官网中“核心概念”的第一个。
想了解vuex中的其他源码分析,欢迎参考我发布的下列文章:
VUEX 3 源码分析——1. 理解State
VUEX 3 源码分析——2. 理解Getter
VUEX 3 源码分析——3. 理解Mutations
VUEX3 源码分析——4. 理解Action
VUEX 3 源码分析——5. 理解Module
VUEX 3 源码分析——6. 理解命名空间namespace
VUEX 3 源码分析——7. 模块的动态注册和卸载
VUEX 3 源码分析——8. 插件开发
VUEX 3 源码分析——9. 严格模式
State的初始化
当我们创建了一个store时,state发生了什么。
假设我们按官网的内容,初始化了一个最基础的store:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
}
// 这里去掉了 mutations 的内容,只关注state
})
- 这里实例上是创建了一个Store类,并将count内容作为参数传入
// ./index.js
import { Store } from './store'
// Vuex.Store 就是这里的Store,它是从 ./store 导入的
export default {
Store,
...
}
// ./store.js
import ModuleCollection from './module/module-collection'
export class Store{
constructor(options = {}) {
...
this._modules = New ModuleCollection(options)
const state = this._modules.root.state
// 可以看出是来自于ModuleCollection类的,那接着看下一段 ModuleCollection 的代码
}
}
// ./module/module-collection.js
import Module from './module'
export default class ModuleCollection {
constructor(rawRootModule) {
this.register([], rawRootModule, false)
}
...
register(path, rawModule, runtime=true) {
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
this.root = newModule
} else {...}
...
}
}
// ./module/module.js
export default class Module {
constructor(rawModule, runtime) {
this._rawModule = rawModule
// 最后在这里处理我们传入的state,Module.state
const rawState = rawModule.state
// 这里能让我们定义的state除了可以是对象,还可以是函数
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
}
- 至此,可以看出store在初始化时,将定义的state内容赋值到了module.state上,
- 同时将module绑定到 ModuleCollection.root,
- 并将实例化的ModuleCollection赋值给Store._modules
- 最后,为了让vue能监测到state的变化并更新响应的组件,调用了vue中的reactive函数,创建了一个响应式对象 store._state = {data: state},代码如下:
// ./store.js
import { resetStoreState } from './store-util'
export class Store {
constructor(options = {}) {
...
this._modules = new ModuleCollection(options)
...
const state = this._modules.root.state
...
// 在这里生成响应式对象 this._state
resetStoreState(this, state)
}
}
// ./store-util.js
import { reactive } from 'vue'
export function resetStoreState(store, state, hot) {
...
store._state = reactive({data: state})
...
}
- 在这里还没有结束,因为如果按上面的内容,我们要使用定义的count属性的话,代码应该是
store._state.data.count - 但实际上是 store.state.count。vuex还做了一些简单的处理,就是定义了一个getter方法,
- 另外还对state设置了setter方法,来阻止用户直接修改state的值
- 下面是store.js文件中目前我们所接触的所有代码
// ./store.js
import ModuleCollection from './module/module-collection'
import { resetStoreState } from './store-util'
export class Store {
constructor(options = {}) {
...
this._modules = new ModuleCollection(options)
const state = this._modules.root.state
resetStoreState(this, state)
}
get state() {
return this._state.data
}
set state(v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
}
- 之前使用vuex时,经常有人说直接修改state的属性会告警且不能修改,原因就在这里。
- 但我们通过源码分析可以发现,vuex其实并没有阻止使用者修改state,只是阻止了开发环境的修改,如果部署到生成环境就可以直接修改且不会告警。至于官方建议使用mutation方式来提交修改,说更明确地追踪到状态的变化,等后续源码分析完后就能深刻地理解了。
mapState 辅助函数
- 先看下官方的示例:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
// 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
- 让我们看下vuex的源码,是如何实现辅助生成计算属性这个功能的,同时还保证了多种定义计算属性的方法,同时既可以传入对象,又可以传入数组。
- 首先 mapState 函数定义在 ./helpers.js 中
- normalizeNamespace是一个高阶函数,它接受一个函数作为参数,并返回一个函数。在它的返回函数中,接受namespace和map作为参数,返回normalizeNamespace接受的函数的执行结果。
- 在normalizeNamespace的代码中,如果namespace不是字符串类型的话,就将两个参数的值互换,这样保证我们传入的第一个参数(实例中的对象或数组)会被赋值给第二个参数。但是我感觉这里一股别扭感,按道理map在前,namespace在后更合理一些,也不用交换两个参数的值了。可能是为了保持某种使用习惯的统一吧。
- 所以 mapState 就是 normalizeNamespace 返回的函数。当按上面官方的例子执行类似 mapState(…)的时候,就是将…作为参数传入normalizeNamespace接受的函数,并执行这个函数,返回结果。
- vuex还有其他地方都有使用这种类似的高阶函数,大致就是接受的参数是一个函数A,同时返回的也是一个函数B,在返回的函数B里执行接受的函数A的操作。
- 之后 normalizeMap 函数将我们传入的state做了格式化处理,这里的代码解释了为什么mapState传入的参数既可以是对象,也可以是数组。
- 格式化参数后states后,遍历新的数组,得到res对象。这个对象的key是我们传参时的对象的key或者数组的每个元素,value是一个函数,或者我们定义的函数。
// ./helper.js
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState() {
let state = this.$store.state
let getters = this.$store.getters
...
// 这里保证我们在使用mapState时,既可以直接使用state的某个属性,
// 也可以函数式地定义来获得某个计算属性
// 同时在使用函数式定义时,
// 还能获得当前组件的相关内容(this),以及store中的state和getters
return typeof val === 'function' ? val.call(this, state, getters) : state[val]
}
})
return res
})
// normalizeNamespace()括号包裹住的整个部分就是normalizeNamespace它接受的参数,也是一个函数,mapState就是它返回的函数
function normalizeNamespace(fn) {
// 下面就是 mapState 会执行的函数内容
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else {...}
return fn(namespace, map)
}
}
function normalizeMap(map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({key, val: key})
: Object.keys(map).map(key => ({key, val: map[key]}))
}
- 所以,mapState是一个高阶函数的返回函数。
- 执行mapState后,会返回一个对象res,如果我们传入mapState的参数是对象,那res的key就是传参对象的key;如果传入的参数是数组,那这个res的key就是数组中的每个元素。
- res的value都是一个待执行的mappedState函数。
- 这个res对象作为当前组件的计算属性。
- 当计算属性所依赖的响应式数据发生变化时,对应的mappedState函数才会执行,然后根据最新的state和getter来计算并返回相应的值。
- 这种设计模式使得状态的映射可以延迟到组件的计算属性被访问时执行。这样做的好处是可以确保状态映射总是基于最新的 state 和 getters,并且可以利用 Vue 的响应式系统来自动更新。
- 同时通过分析源码,还发现了除了可以利用state以外,还能访问到getters的内容,这个是官网没写的,例如,我们可以这样修改官方的例子:
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
// 函数还可以接受getters,例如:
countPlusLocalState_2(state, getters) {
return state.count + this.localCount + getters.doubleCount
}
})
}
- 最后就是官网的“对象展开运算符”
- 只要理解了mapState是返回一个对象,以及展开运算符的意义,就知道必然能这样使用了
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
以上就是官网上state的相关源码,下一遍会分析getters的实现源码。