拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vuex Getters 完全指南:从派生状态计算到源码级缓存机制解析

Vuex Getters 完全指南从派生状态计算到源码级缓存机制解析【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuexVuex 的 Getters 用于基于 store 中的 state 计算派生状态derived state可视为 store 的计算属性computed properties for stores。本文以官方指南 docs/guide/getters.md 为核心骨架结合本仓库 src/store-util.js 与 src/store.js 的源码实现、test/unit 下的测试用例系统讲解 Getters 的定义方式、两种访问风格属性式与方法式、mapGetters辅助函数、模块化与命名空间场景以及其底层缓存与响应式机制帮助你写出可复用、高性能的派生状态逻辑。Getters 要解决的问题避免重复计算与复制代码当多个组件需要基于同一份 state 计算派生数据时直接在组件内写计算逻辑会产生明显问题。比如在每个组件里重复编写如下代码computed: { doneTodosCount () { return this.$store.state.todos.filter(todo todo.done).length } }如果多个组件都需要这个结果就得重复复制这段函数或者抽离成共享 helper 再在多个地方 import——两种方案都不理想前者破坏 DRY 原则、后期维护成本高后者虽然避免了复制但计算逻辑游离在状态管理之外难以被调试工具追踪。Vuex 的答案是让 store 自己提供派生状态能力在 store 中定义getters把从 state 推导数据的逻辑集中管理任何组件都能按需取用。::: warning 注意 在 Vue 3.0 中getter 的结果不会像 Vue 的计算属性那样被缓存这是官方已知问题需要 Vue 3.1 版本才能解决详见 PR #1878 的讨论。本文后续会结合源码说明该问题的本质。 :::定义第一个 Getterstate 作为第一个参数Getters 接收state 作为第 1 个参数在createStore的配置对象中定义import { createStore } from vuex const store createStore({ state: { todos: [ { id: 1, text: ..., done: true }, { id: 2, text: ..., done: false } ] }, getters: { doneTodos (state) { return state.todos.filter(todo todo.done) } } })doneTodos根据state.todos过滤出已完成项组件无需再关心过滤逻辑也无需知道todos在 state 中的具体结构。属性式访问Property-Style Access定义后的 getters 会暴露在store.getters对象上以属性的方式直接取值store.getters.doneTodos // - [{ id: 1, text: ..., done: true }]getters 作为第二个参数getter 之间相互组合Getters 还会接收其他 getters 作为第 2 个参数从而支持 getter 之间的组合复用getters: { // ... doneTodosCount (state, getters) { return getters.doneTodos.length } }store.getters.doneTodosCount // - 1doneTodosCount直接复用了doneTodos的结果而不是重新 filter 一次。这种getter 依赖 getter的组合方式是构建派生状态层的推荐做法。在组件中使用在任意组件中通过this.$store.getters访问computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } }注意属性式访问的 getter 会作为 Vue 响应式系统的一部分被缓存。也就是说只要其依赖的 state 未变化多次访问返回的都是缓存结果这与组件computed的行为一致。官方文档明确指出getters accessed as properties are cached as part of Vues reactivity system。源码解析属性式 getter 为什么会被缓存从源码看属性式 getter 的缓存并非凭空而来而是建立在 Vue 的computed之上。在 src/store-util.js 的resetStoreState函数中// src/store-util.js 第 30-58 行节选 export function resetStoreState (store, state, hot) { const oldState store._state const oldScope store._scope // 重置 store 公开 getters 与本地 getters 缓存 store.getters {} store._makeLocalGettersCache Object.create(null) const wrappedGetters store._wrappedGetters const computedObj {} const computedCache {} const scope effectScope(true) scope.run(() { forEachValue(wrappedGetters, (fn, key) { // 利用 computed 的懒缓存机制 computedObj[key] partial(fn, store) computedCache[key] computed(() computedObj[key]()) Object.defineProperty(store.getters, key, { get: () computedCache[key].value, enumerable: true }) }) }) // ... }关键实现链路如下registerGetter包装原始 gettersrc/store-util.js 第 254-269 行每个用户定义的 getter 被包装为wrappedGetter(store)调用时依次传入local.state本地 state、local.getters本地 getters、store.state根 state、store.getters根 getters四个参数。computed接管缓存computedCache[key] computed(() computedObj[key]())把包装后的 getter 塞进 Vue 的computed中。store.getters[key]的 getter 访问器返回computedCache[key].value因此只要依赖的 state 未变重复访问 getter 都会命中 computed 的缓存。EffectScope 隔离所有 getter 的 computed 被放入effectScope(true)detached scope中执行这样组件卸载时不会销毁 getters 的计算。这一点在 test/unit/modules.spec.js 的 should keep getters when component gets destroyed 用例中得到验证CompA组件销毁后getter 依然能够响应store.commit并重新求值。这里也顺带解释了官方警告的根源缓存生效的前提是 getter 内部逻辑能够被 Vue 的响应式依赖追踪捕获。在 Vue 3.0 中由于底层实现限制部分场景下 getter 的结果未能如computed那样正确缓存属于版本缺陷官方已在后续版本修复。方法式访问Method-Style Access给 getter 传参属性式 getter 无法接收动态参数。当你需要按条件查询 store 中的数据时可以让 getter返回一个函数调用时再传参getters: { // ... getTodoById: (state) (id) { return state.todos.find(todo todo.id id) } }store.getters.getTodoById(2) // - { id: 2, text: ..., done: false }这种柯里化写法让 getter 变成带参数的查询方法特别适合按 id 查询列表项按关键字过滤这类场景。::: warning 注意 方法式访问的 getter每次调用都会重新执行结果不会被缓存。官方文档明确说明getters accessed via methods will run each time you call them, and the result is not cached.如果你在渲染函数或模板中频繁调用带参 getter需要注意其性能开销必要时可考虑在组件内用computed做二次缓存。 :::为什么方法式访问无法缓存对比源码可以找到原因属性式 getter 的包装函数返回值会被外层computed捕获依赖并缓存而方法式 getter 返回的是一个函数computedCache[key].value缓存的是这个函数本身。函数每次被调用时内部对state的读取发生在computed的求值上下文之外Vue 无法建立响应式依赖跟踪自然也就无从缓存结果。mapGetters 辅助函数批量映射到组件手写this.$store.getters.xxx在 getter 变多时会变得啰嗦。mapGetters辅助函数把 store getters直接映射为组件的本地计算属性import { mapGetters } from vuex export default { // ... computed: { // 使用对象展开运算符将 getters 混入 computed ...mapGetters([ doneTodosCount, anotherGetter // ... ]) } }映射后组件内可以直接使用this.doneTodosCount、this.anotherGetter与普通 computed 无异。重命名映射对象语法如果想把 getter 映射到不同的本地名称使用对象形式...mapGetters({ // 将 this.doneCount 映射为 this.$store.getters.doneTodosCount doneCount: doneTodosCount })对象语法在 getter 名与组件语义命名不一致时非常实用。源码解析mapGetters 的实现mapGetters定义在 src/helpers.js 第 72-94 行其核心逻辑为export const mapGetters normalizeNamespace((namespace, getters) { const res {} normalizeMap(getters).forEach(({ key, val }) { // 命名空间已由 normalizeNamespace 归一化 val namespace val res[key] function mappedGetter () { if (namespace !getModuleByNamespace(this.$store, mapGetters, namespace)) { return } if (__DEV__ !(val in this.$store.getters)) { console.error([vuex] unknown getter: ${val}) return } return this.$store.getters[val] } // 为 devtools 标记 vuex getter res[key].vuex true }) return res })几个值得注意的实现细节normalizeMapsrc/helpers.js 第 145-152 行统一处理数组与对象两种输入数组[a]生成{ key: a, val: a }对象{ a: b }生成{ key: a, val: b }。生成的是惰性读取的函数mappedGetter在组件计算属性求值时才真正访问this.$store.getters[val]因此 getter 的响应式依赖天然成立——mapGetters映射出来的属性与手写 computed 一样会自动更新。开发模式下如果 getter 不存在会输出[vuex] unknown getter: ${val}错误提示便于快速定位拼写问题。每个映射函数被标记res[key].vuex true供 devtools 识别。对应测试见 test/unit/helpers.spec.js数组形式mapGetters([hasAny, negative])与对象形式mapGetters({ a: hasAny, b: negative })均有覆盖且验证了 commit 改变 state 后映射属性同步更新。模块中的 Getter本地状态与根状态store 拆分为模块后getter 的行为遵循模块化规则详见 docs/guide/modules.md本地 state 优先在模块的 getter 中第一个参数是模块的本地 stateconst moduleA { state: () ({ count: 0 }), getters: { doubleCount (state) { return state.count * 2 // state 是模块本地 state } } }访问根 state第 3、4 个参数当需要读取根 state 或根 getters 时getter 函数的第 3 个参数是rootState第 4 个参数是rootGettersconst moduleA { // ... getters: { sumWithRootCount (state, getters, rootState) { return state.count rootState.count } } }这与 src/store-util.js 中registerGetter的包装逻辑一一对应store._wrappedGetters[type] function wrappedGetter (store) { return rawGetter( local.state, // 本地 state local.getters, // 本地 getters store.state, // 根 state store.getters // 根 getters ) }也就是说getter 函数签名的四个参数顺序为(localState, localGetters, rootState, rootGetters)。命名空间namespaced下的 getter模块标记namespaced: true后其 getters 会自动按注册路径加前缀docs/guide/modules.md 第 89-132 行const store createStore({ modules: { account: { namespaced: true, getters: { isAdmin () { ... } // - getters[account/isAdmin] }, modules: { myPage: { getters: { profile () { ... } // - getters[account/profile]继承父命名空间 } }, posts: { namespaced: true, getters: { popular () { ... } // - getters[account/posts/popular]进一步嵌套 } } } } } })未加命名空间的模块其 getters 默认注册在全局命名空间下此时不同模块中不能定义同名 getter否则会在开发模式报[vuex] duplicate getter key: ${type}错误见 src/store-util.js 第 254-260 行registerGetter的查重逻辑。命名空间模块内部getters 收到的是本地化localized的 getters想要访问全局 getters 则通过第 4 个参数rootGettersmodules: { foo: { namespaced: true, getters: { someGetter (state, getters, rootState, rootGetters) { getters.someOtherGetter // - foo/someOtherGetter rootGetters.someOtherGetter // - someOtherGetter rootGetters[bar/someOtherGetter] // - bar/someOtherGetter }, someOtherGetter: state { ... } } } }从源码看本地化 getters 由makeLocalGetterssrc/store-util.js 第 197-220 行实现它会遍历store.getters中所有带该命名空间前缀的键剥离前缀后通过Object.defineProperty定义为代理属性并缓存在store._makeLocalGettersCache[namespace]中避免重复构建。命名空间下的 mapGettersmapGetters第一个参数可以传入命名空间批量映射命名空间模块内的 getterscomputed: { ...mapGetters(some/nested/module, [ someGetter // - this[someGetter]等价于 this.$store.getters[some/nested/module/someGetter] ]) }如果命名空间前缀较长还可以用createNamespacedHelpers预绑定import { createNamespacedHelpers } from vuex const { mapGetters } createNamespacedHelpers(some/nested/module) export default { computed: { ...mapGetters([someGetter]) } }createNamespacedHelpers在 src/helpers.js 第 131-136 行实现本质是对四个 map 辅助函数做bind(null, namespace)预绑定而mapGetters内部通过normalizeNamespace自动为命名空间补全末尾的/若缺失随后在取 getter 时拼接为完整名称namespace val。对应测试见 test/unit/helpers.spec.js 的 mapGetters (with namespace) 用例以及 test/unit/modules.spec.js 中动态注册带命名空间模块后store.getters[a/foo]的断言。源码视角getter 的完整生命周期综合本仓库源码一个 getter 从定义到使用的完整链路如下注册收集createStore构造时src/store.js 第 15-76 行installModule遍历各模块的 getters调用registerGetter将每个 getter 包装后存入store._wrappedGetters键为namespace key。计算属性化resetStoreState将每个_wrappedGetters包装函数塞入computed并把store.getters[key]定义为读取computedCache[key].value的访问器属性。使用与追踪组件通过store.getters属性式或mapGetters映射内部同样是访问store.getters读取state 变化时computed 依赖被触发getter 自动重算。动态模块registerModule会重新执行installModule与resetStoreState动态模块的 getters 随即生效unregisterModule通过resetStore重建旧 getters 随之移除——test/unit/modules.spec.js 验证了unregisterModule后store.getters.a变为undefined。热更新hotUpdate触发resetStore后重建全部 getters 的计算实现不刷新页面的状态与派生逻辑热替换详见 docs/guide/hot-reload.md。最佳实践与常见陷阱优先属性式 getter能被 Vue 响应式缓存性能更好写法更简洁只有需要动态传参时才使用方法式。getter 组合复用通过第二个参数getters让 getter 依赖 getter避免重复实现相同过滤/聚合逻辑。保持 getter 纯函数getter 只应基于参数state、getters计算返回值不应产生副作用如修改 state、发起请求。派生逻辑放 getter状态变更走 mutation/action。方法式 getter 高频调用需警惕每次调用都重新执行若在模板中高频调用且计算较重考虑在组件 computed 中做一层缓存。未命名空间模块勿重名多个非 namespaced 模块中定义同名 getter 会触发duplicate getter key错误生产构建中该错误仅静默跳过注册见registerGetter的__DEV__判断易产生难排查的隐性 bug。命名空间场景善用 mapGetters 第一参数长命名空间下结合createNamespacedHelpers可显著降低代码噪音。小结Getters 是 Vuex 派生状态层的核心它以 state 为输入、以声明式函数产出可复用数据属性式访问获得响应式缓存、方法式访问支持动态参数mapGetters让组件接入零成本。结合本仓库 src/store-util.js 的源码可以看到其缓存能力建立在 Vuecomputed与 EffectScope 之上模块化与命名空间则通过_wrappedGetters的键前缀和makeLocalGetters的代理机制实现。掌握这些原理你就能在大型应用中合理地组织 getter 层次避免重复计算与命名冲突。getter 的完整 API 参考可继续查阅 docs/api/index.md。【免费下载链接】vuex️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门