这是对vuex3.x版本的源码分析。
本次分析会按以下方法进行:
- 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
- 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
- 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。
本章只讲解vuex中的Getter,这也是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. 模块的动态注册和卸载
Getter的初始化
以官网的例子为store的初始化内容:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
在store.js中,将state绑定到this._modules.root.state之后,执行了installModule函数用于getters的处理。
// ./store.js
import ModuleCollection from './module/module-collection'
import { installModule, resetStoreState } from './store-util'
export class Store {
constructor(options = {}) {
...
this._wrappedGetters = Object.create(null) // 存放getters
this._modules = new ModuleCollection(options)
const store = this
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
// this._modules.root 包含我们初始化Store时定义的所有内容
resetStoreState(this, state)
}
}
- 在 installModule 中,调用了 forEachGetter 方法,可以看到 forEachGetter 方法接受一个函数fn作为参数。
- Module类的 forEachGetter 方法中,将例子中定义的getters内容按key-value进行遍历,以value, key的顺序传入函数fn中并执行
// ./store-util.js
export function installModule(store, rootState, path, module, hot) {
...
const local = makeLocalContext(store, namespece, path)
// 目前的local可以看做官网例子中,实例化时传入的对象内容的拷贝
module.forEachGetter((getter, key) => {
// 还未涉及到 namespace 的内容,这里的 namespace 都可以看做空字符串
const namespacedType = namespace + key
registerGetter(store, namespacedTyep, getter, local)
}
}
function makeLocalContext(store, namespace, path) {
const local = {}
Object.defineProperties(local, {
getters: {get: () => store.getters},
state: {get: () => getNestedState(store.state, path)}
}
}
// ./module/modules.js
export default class Module {
...
forEachGetter(fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
}
// ./util.js
export function forEachValue(obj, fn) {
object.keys(obj).forEach(key => fn(obj[key], key))
}
- 在遍历 getters 的过程中,调用 registerGetter 函数
- registerGetter函数将实例中定义的getter,按key-value格式赋值给了 store._wrappedGetters
- 其中key是定义的getter属性名或方法名,value是一个接受store参数的高阶函数,它返回rawGetter函数的执行结果
- rawGetter就是用户自己定义的getter,rawGetter函数接受4个参数(这也是官网中,用户对getter的初始化定义时,为什么能传入getters以及全局state和getters的原因)
// store-util.js
// 参数对应的内容依次是 store=store实例,type=getter中的key,
// rawGetter=getter中的value即用户实际定义的getter函数,local目前可看做 store 对象的拷贝
function registerGetter(store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
...
return // 如果已经注册则不做处理,直接返回
}
store._wrappedGetters[type] = function wrappedGetter(store) {
return rawGetter(
local.state, local.getters,
store.state, store.getters
)
}
}
- 至此,我们定义的getters内容,按key-value的格式绑定到了 store._wrappedGetters对象上。
- 之后,通过store.js中的resetStoreState(this, state)函数,暴露褚给外界的Getter。
- 在 resetStoreState 函数中,首先通过VUE内部的effectScope API 创建一个scope
- 再对 wrappedGetters 进行key-value遍历,将value的执行结果用 VUE 的 computed API 转变成 响应式的计算属性并赋值给缓存变量computedCache[key]
- 上述过程中使用了VUE的相关API,例如 effectScope 和 computed:
- VUE effectScope API: 用于组织副作用的高级 API。副作用是指那些会产生副作用的响应式状态变化,如 watch、computed 以及使用 reactive 或 ref 定义的响应式状态的变化。effectScope 允许开发者在一个封装的作用域内管理这些副作用,使得副作用的生命周期管理变得更加简单和集中。
- VUE computed API::创建一个VUE的响应式计算属性
// ./store-util.js
import { reactive, computed, watch, effectScope } from 'vue'
export function resetStoreState(stoer, state, hot) {
...
const wrappedGetters = store._wrappedGetters
const computedObj = {}
const computedCache = {} // 用于缓存
const scope = effectScope(true);
scope.run(() => {
forEachValue(wrappedGetters, (fn, key) => {
computedObj[key] = partial(fn, store)
computedCache[key] = computed(() => computedObj[key]())
Object.defineProperty(store.getters, key, {
get: () => computedCache[key].value,
enumerable: true // 默认为false
})
})
})
}
// util.js
export function partial(fn, arg) {
return function() {
return fn(arg)
}
}
mapGetters函数
- mapGetters函数和mapState函数的实现逻辑大体一致
- normalizeNamespace 是一个高阶函数,它接收一个函数(fn)作为参数,并返回一个新函数,这个新函数接收两个参数:namespace 和 map。
- normalizeMap 将数组或对象转换成统一格式(类似 [{ key: key, value: value }])
- 最终 mapGetters 执行后返回一个特定的字典结构的res
- res的key是getter的名字或者自己定义的名字,value对应一个函数,这个函数返回this.$store.getters[val]。这里的this上下文就对应vue组件的节点,从而实现混入效果.
- 由于得到的res是解构在组件的计算属性里面的,所以可以通过this.key来调用。
// ./helper.js
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({key, val}) => { // 解构赋值
val = namespace + val
res[key] = function mappedGetter() {
...
return this.$store.getters[val]
}
res[key].vuex = true
}
return res
})
function normalizeNamespace(fn) {
return (namespace, map) {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if { ... } // 局部命名空间处理
return fn(namespace, map)
}
}
// normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
// normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
function normalizeMap(map) {
...
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
最后,对官网中“通过方法访问”的分析
- 官网中,对getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果,让我们从源码层分析其原因。
- 这里的 getTodoById 对应的value 是一个柯里化的函数,它接受state作为参数,返回一个接受参数为id的函数,在返回的函数里,执行了getter需要的操作。
- 所以调用getTodoById时,实际返回的是一个函数。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
- 我们通过上面的源码分析,已经知道getters在初始化时,将对应的值存放到了computedCache[key]中,同时通过调用VUE 的 computed API 转变成响应式的计算属性。
- 大致源码如下:
// ./store-utils.js
export function resetStoreState(store, state, hot) {
...
scope.run(() => {
forEachValue(wrappedGetters, (fn, key) => {
computedObj[key] = partial(fn, store) // partial 返回一个函数,函数内执行 fn(store)
if (typeof computedObj[key]() === function) {
// todo somethings
} else {
computedCache[key] = computed(() => computedObj[key]())
}
//computedCache[key] = computed(() => computedObj[key]()) // 缓存 fn(store)的执行结果
Object.defineProperty(store.getters, key, {
get: () => computedCache[key].value,
enumerable: true
})
})
})
...
}
- 由于 store.getters.getTodoById 返回的是一个函数,所以 computedCache[key] 缓存是一个函数,并没有缓存函数中有关的数据内容。所以每次调用时,得到的都是这个函数的执行结果。相当于新生成了一个函数并执行。
以上就是官网上getters的相关源码,下一遍会分析Mutations的实现源码。