阐述 Vuex 源码中 `getters` 的实现,包括其依赖收集和缓存机制。

各位观众老爷,晚上好!今儿个咱们不聊八卦,聊点硬核的,抠一抠 Vuex 源码里 getters 这玩意儿的实现,看看它咋做依赖收集,又咋玩转缓存的。准备好了吗?老司机要发车了!

开场白:Getters,这磨人的小妖精

getters 在 Vuex 里扮演着什么角色?简单说,它就是个计算属性的集中营。当你需要从 state 派生出一些数据,并且这些数据可能被多个组件用到时,getters 就派上用场了。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 10
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    // 还能接受其他 getters 作为参数
    tripleCount: (state, getters) => getters.doubleCount + state.count
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

export default store

瞧见没?doubleCounttripleCount 都是 gettersdoubleCount 直接从 state 拿数据算, tripleCount 还能用上其他的 getters。 这就像厨房里的食材和菜谱,state 是食材,getters 是菜谱,你拿着食材,照着菜谱就能做出美味佳肴。

源码探秘:Getters 是怎么炼成的?

想知道 getters 的实现,得扒一扒 Vuex 的源码。 别怕,我带着你,保证不迷路。

  1. Store 的初始化:Getter 的登记

    Store 实例化的时候,Vuex 会把我们定义的 getters 登记在册。 具体来说,是在 Store 的构造函数里调用 resetStoreVM 方法,这个方法里会创建Vue实例,并将 getters 转换为计算属性。

    // src/store.js
    constructor (options = {}) {
     // ...
     this.strict = options.strict
     this._committing = false
     this._actions = Object.create(null)
     this._actionSubscribers = []
     this._mutations = Object.create(null)
     this._wrappedGetters = Object.create(null)
     this._modules = new ModuleCollection(options)
     this._modulesNamespaceMap = Object.create(null)
     this._subscribers = []
     this._watcherVM = new Vue()
     this._makeLocalGettersCache = Object.create(null)
    
     const state = this._modules.root.state
    
     installModule(this, state, [], this._modules.root)
    
     // initialize the store vm, which is the root of the whole vuex
     // instance.
     resetStoreVM(this, state)
    
     // ...
    }

    resetStoreVM 函数负责创建 Vue 实例,并将 getters 转换为计算属性。

    // src/store.js
    function resetStoreVM (store, state, hot) {
     const oldVm = store._vm
    
     // bind store public getters
     store.getters = {}
     const wrappedGetters = store._wrappedGetters
     const computed = {}
     forEachValue(wrappedGetters, (fn, key) => {
       // use computed to leverage its lazy-caching mechanism
       computed[key] = partial(fn, store)
       Object.defineProperty(store.getters, key, {
         get: () => store._vm[key],
         enumerable: true // for devtools
       })
     })
    
     // use a Vue instance to store the state tree
     // suppress warnings just in case the user has added
     // some funky global mixins
     // use silent mode since we expose these vm.s internally as
     // private properties.
     const silent = Vue.config.silent
     Vue.config.silent = true
     store._vm = new Vue({
       data: {
         $$state: state
       },
       computed
     })
     Vue.config.silent = silent
    
     // ...
    }

    这段代码做了几件事:

    • 遍历 wrappedGetters,这是经过包装的 getters 函数,目的是为了传入 store 实例作为参数。
    • 将每个 getter 函数转换为一个计算属性,放进 computed 对象里。
    • store.getters 定义属性,这些属性的 get 方法实际上是从 store._vm 上读取对应的值。 store._vm 就是一个 Vue 实例,它的 computed 属性里包含了我们定义的 getters
  2. Getter 的包装:注入 Store 实例

    wrappedGetters 是怎么来的?它是在 installModule 函数里生成的。这个函数负责安装模块,包括根模块和子模块。

    // src/module/module-collection.js
    export function installModule (store, rootState, path, module, hot) {
     const isRoot = !path.length
     const namespace = store._modules.getNamespace(path)
    
     // register in namespace map
     if (module.namespaced) {
       if (store._modulesNamespaceMap[namespace] && __DEV__) {
         console.error(`[vuex] duplicate namespace ${namespace} found for module [${path.join('/')}]`)
       }
       store._modulesNamespaceMap[namespace] = module
     }
    
     // set state
     if (!isRoot && !hot) {
       const parentState = getNestedState(rootState, path.slice(0, -1))
       const moduleName = path[path.length - 1]
       store._withCommit(() => {
         Vue.set(parentState, moduleName, module.state)
       })
     }
    
     const local = module.context = makeLocalContext(store, namespace, path)
    
     const getters = module.getters
     if (getters) {
       forEachValue(getters, (getter, key) => {
         if (__DEV__ && store._wrappedGetters[namespace + key]) {
           console.error(`[vuex] duplicate getter key ${namespace + key} found in namespace ${namespace}.`)
         }
         store._wrappedGetters[namespace + key] = function (store) {
           return getter(
             local.state, // local state
             local.getters, // local getters
             store.rootState, // root state
             store.rootGetters // root getters
           )
         }
       })
     }
    
     // ...
    }

    关键代码:

    store._wrappedGetters[namespace + key] = function (store) {
     return getter(
       local.state, // local state
       local.getters, // local getters
       store.rootState, // root state
       store.rootGetters // root getters
     )
    }

    这行代码把原始的 getter 函数包装了一下,目的是为了在执行 getter 函数的时候,能够传入正确的参数:

    • local.state: 当前模块的 state
    • local.getters: 当前模块的 getters
    • store.rootState: 根模块的 state
    • store.rootGetters: 根模块的 getters

    这样,即使是在模块化的 Vuex 应用里,getters 也能正确访问到所需的数据。

依赖收集:Getter 的监听之道

getters 的一大特点就是会自动进行依赖收集。啥意思?就是说,当 getter 函数依赖的 state 发生变化时,Vue 会自动重新计算 getter 的值。

这背后的功臣就是 Vue 的响应式系统。 当 getter 函数被访问时(也就是我们通过 store.getters.doubleCount 访问 getter 的值时),Vue 会记录下这个 getter 函数依赖了哪些 state。 一旦这些 state 发生变化,Vue 就会通知这个 getter 函数重新计算。

还记得 resetStoreVM 函数里把 getters 转换成了计算属性吗?计算属性本身就具有依赖收集的功能。当计算属性的值被访问时,Vue 会自动追踪它所依赖的响应式数据。

缓存机制:Getter 的懒加载策略

getters 不仅仅会进行依赖收集,还具有缓存机制。 也就是说,只有当 getter 依赖的 state 发生变化时,才会重新计算 getter 的值。 否则,直接返回缓存的值。

这又是计算属性的功劳。 计算属性具有懒加载特性,只有当它被访问时才会计算,并且会缓存计算结果。 下次访问时,如果依赖没有发生变化,直接返回缓存值,避免重复计算。

总结:Getters 的运作流程

咱们来梳理一下 getters 的运作流程:

  1. 定义: 在 Vuex 的 store 中定义 getters
  2. 包装: Vuex 会对 getters 进行包装,注入 store 实例以及模块相关的 stategetters
  3. 登记: Vuex 会将包装后的 getters 转换为计算属性,注册到 Vue 实例上。
  4. 访问: 当我们访问 store.getters.xxx 时,实际上是在访问 Vue 实例上的计算属性。
  5. 依赖收集: Vue 的响应式系统会自动追踪 getter 函数所依赖的 state
  6. 缓存: 计算属性会缓存 getter 的计算结果。
  7. 更新:getter 依赖的 state 发生变化时,Vue 会通知计算属性重新计算,并更新缓存。

用一张表格来总结一下:

步骤 描述 涉及的源码
定义 在 Vuex 的 store 中定义 getters store.js
包装 Vuex 会对 getters 进行包装,注入 store 实例以及模块相关的 stategetters src/module/module-collection.js -> installModule
登记 Vuex 会将包装后的 getters 转换为计算属性,注册到 Vue 实例上。 src/store.js -> resetStoreVM
访问 当我们访问 store.getters.xxx 时,实际上是在访问 Vue 实例上的计算属性。 N/A
依赖收集 Vue 的响应式系统会自动追踪 getter 函数所依赖的 state Vue 的响应式系统 (不在 Vuex 源码里,而是 Vue 的核心机制)
缓存 计算属性会缓存 getter 的计算结果。 Vue 的计算属性 (不在 Vuex 源码里,而是 Vue 的核心机制)
更新 getter 依赖的 state 发生变化时,Vue 会通知计算属性重新计算,并更新缓存。 Vue 的响应式系统 + 计算属性 (不在 Vuex 源码里,而是 Vue 的核心机制)

Getters 的妙用:一些栗子

光说不练假把式,来几个实际的例子:

  1. 购物车总价:

    // store.js
    state: {
     cart: [
       { id: 1, price: 10, quantity: 2 },
       { id: 2, price: 20, quantity: 1 }
     ]
    },
    getters: {
     totalPrice: (state) => {
       return state.cart.reduce((total, item) => total + item.price * item.quantity, 0)
     }
    }

    在组件里:

    <template>
     <div>
       总价:{{ totalPrice }}
     </div>
    </template>
    
    <script>
    import { mapGetters } from 'vuex'
    
    export default {
     computed: {
       ...mapGetters(['totalPrice'])
     }
    }
    </script>
  2. 过滤列表:

    // store.js
    state: {
     todos: [
       { id: 1, text: '吃饭', done: true },
       { id: 2, text: '睡觉', done: false },
       { id: 3, text: '打豆豆', done: false }
     ]
    },
    getters: {
     doneTodos: (state) => {
       return state.todos.filter(todo => todo.done)
     }
    }

    在组件里:

    <template>
     <ul>
       <li v-for="todo in doneTodos" :key="todo.id">{{ todo.text }}</li>
     </ul>
    </template>
    
    <script>
    import { mapGetters } from 'vuex'
    
    export default {
     computed: {
       ...mapGetters(['doneTodos'])
     }
    }
    </script>

    这些例子展示了 getters 的强大之处,它可以将复杂的计算逻辑从组件中抽离出来,集中管理,并且具有依赖收集和缓存的特性,提高了应用的性能。

总结陈词:Getters,Vuex 的得力助手

getters 是 Vuex 中一个非常重要的概念,它允许我们从 state 中派生出新的数据,并且具有依赖收集和缓存机制。 理解 getters 的实现原理,可以帮助我们更好地利用 Vuex,编写出更高效、更可维护的应用。

下次面试的时候,如果面试官问你 Vuex 的 getters 是怎么实现的,你就可以把今天学到的知识滔滔不绝地讲给他听,保证让他对你刮目相看!

好了,今天的讲座就到这里,感谢各位的收听! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注