VUEX 3.x源码分析——2. 理解Getter

这是对vuex3.x版本的源码分析。
本次分析会按以下方法进行:

  1. 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
  2. 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
  3. 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。

本章只讲解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的实现源码。

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值