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

好嘞,各位观众老爷,今天咱们不聊风花雪月,就来扒一扒 Vuex 源码里那个让人又爱又恨的 getters。别害怕,保证通俗易懂,让大家听完后都能对着源码嘿嘿一笑,说一句:“原来你小子是这么玩的!”

咱们今天主要讲两件事:

  1. getters 的实现原理:它到底是怎么蹦出来的,又是怎么被 Vue 组件用上的。
  2. getters 的依赖收集和缓存机制:Vuex 为了性能,在 getters 身上动了哪些手脚。

准备好了吗? Let’s go!

一、 getters 的诞生记:从定义到使用

首先,咱们回忆一下,getters 在 Vuex 里是怎么定义的?

const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount: (state) => state.count * 2,
    moreThanTen: (state) => state.count > 10
  }
})

很简单,就是一个对象,key 是 getter 的名字,value 是一个函数,这个函数接收 state 作为参数,并返回计算后的值。

那么,Vuex 是怎么把这些定义好的 getters 变成可以在组件里使用的东西呢?

1. 初始化 getters 对象

Vuex.Store 构造函数里,会调用 resetStoreVM 函数,这个函数会初始化 Vue 的响应式系统,并且会调用 installModule 函数来处理模块。在 installModule 函数中,会处理 getters 选项:

function installModule (store, rootState, path, module, hot) {
  // ... 其他代码

  // 处理 getters
  if (module.getters) {
    forEachValue(module.getters, (getter, key) => {
      registerGetter(store, key, getter, module)
    })
  }

  // ... 其他代码
}

这个 forEachValue 函数就是遍历 module.getters 对象,然后对每一个 getter 调用 registerGetter 函数。

2. registerGetter 函数:核心逻辑

registerGetter 函数才是真正把 getter 注册到 Vuex 实例上的关键:

function registerGetter (store, key, getter, module) {
  if (store._wrappedGetters[key]) {
    console.error(`[vuex] duplicate getter key: ${key}`)
    return
  }
  store._wrappedGetters[key] = function wrappedGetter (store) {
    return getter(
      module.namespaced
        ? getNestedState(store.state, module.path)
        : store.state, // local state
      store.getters, // getters
      store.rootState // root state
    )
  }
}

这个函数做了几件事:

  • 检查重名: 如果已经有同名的 getter,就报错。
  • 包装 getter 函数: 把原始的 getter 函数包装成 wrappedGetter 函数,这个函数接收 store 作为参数,然后调用原始的 getter 函数,并传入 stategettersrootState。 注意这里传入的 state 可能是模块的局部 state,也可能是根 state,取决于模块是否开启了命名空间。
  • 存储包装后的 getter: 把包装后的 wrappedGetter 函数存储到 store._wrappedGetters 对象上。

3. 将 getters 挂载到 Vue 实例上

resetStoreVM 函数中,会创建一个 Vue 实例,并将 getters 挂载到这个 Vue 实例的 computed 属性上:

function resetStoreVM (store, state, hot) {
  // ... 其他代码

  const computed = {}
  forEachValue(store.getters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = partial(fn, store)
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  // ...
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

  // ... 其他代码
}

这个 forEachValue 函数会遍历 store.getters 对象,然后对每一个 getter 执行以下操作:

  • partial(fn, store) 创建一个偏函数,把 store 作为参数预先传入 fn 函数,也就是我们前面包装过的 wrappedGetter 函数。 这样,当这个偏函数被调用时,就会自动传入 store 参数。
  • computed[key] = partial(fn, store) 把这个偏函数赋值给 computed 对象,key 就是 getter 的名字。

最后,创建一个 Vue 实例,并将 computed 对象作为 computed 属性传入。 这样,Vue 就会自动为每一个 getter 创建一个计算属性。

4. 在组件中使用 getters

现在,我们就可以在组件中使用 getters 了:

<template>
  <div>
    <p>Count: {{ $store.state.count }}</p>
    <p>Double Count: {{ $store.getters.doubleCount }}</p>
    <p>More Than Ten: {{ $store.getters.moreThanTen }}</p>
  </div>
</template>

在组件中,我们可以通过 $store.getters.getterName 来访问 getter。 实际上,这相当于访问 Vue 实例上的计算属性。 当计算属性被访问时,Vue 会自动调用我们前面创建的偏函数,也就是 wrappedGetter 函数,并传入 store 参数。 wrappedGetter 函数会调用原始的 getter 函数,并传入 stategettersrootState,最终返回计算后的值。

总结一下:

步骤 描述 涉及的函数/对象
1 初始化 getters 对象 Vuex.Store, resetStoreVM, installModule
2 注册 getter:包装 getter 函数,存储到 store._wrappedGetters registerGetter
3 getters 挂载到 Vue 实例的 computed 属性上 resetStoreVM, partial
4 在组件中使用 getters:访问 Vue 实例上的计算属性 $store.getters

二、 getters 的依赖收集和缓存机制:Vuex 的小心机

现在,我们已经知道 getters 是怎么被创建和使用的了。 但是,还有一个很重要的问题:Vuex 是怎么优化 getters 的性能的?

答案就是:依赖收集和缓存机制

1. 依赖收集

当我们在 getter 函数中使用 state 或其他 getters 时,Vue 会自动进行依赖收集。 也就是说,Vue 会记录下这个 getter 函数依赖了哪些 state 或其他 getters

例如,我们有以下 getter:

getters: {
  doubleCount: (state) => state.count * 2,
  tripleCount: (state, getters) => getters.doubleCount + state.count
}
  • doubleCount 依赖于 state.count
  • tripleCount 依赖于 state.countgetters.doubleCount

state.count 发生变化时,Vue 会自动通知 doubleCounttripleCount 这两个 getter 函数重新计算。 这就是依赖收集的作用。

依赖收集是怎么实现的呢?

这就要归功于 Vue 的响应式系统了。 当我们在 getter 函数中访问 state 或其他 getters 时,Vue 会自动调用 track 函数来追踪依赖关系。 track 函数会将当前的 watcher(也就是 getter 对应的计算属性 watcher)添加到 state 或其他 getters 对应的依赖列表中。

2. 缓存机制

Vue 的计算属性具有缓存机制。 也就是说,当计算属性被访问时,Vue 会先检查缓存中是否已经有计算结果。 如果有,就直接返回缓存中的结果,而不会重新计算。

只有当计算属性依赖的 state 或其他 getters 发生变化时,Vue 才会清空缓存,并重新计算。

缓存机制是怎么实现的呢?

Vue 的计算属性 watcher 会维护一个 dirty 属性。 当计算属性依赖的 state 或其他 getters 发生变化时,Vue 会将 dirty 属性设置为 true

当计算属性被访问时,Vue 会先检查 dirty 属性是否为 true。 如果是,就重新计算,并将 dirty 属性设置为 false,然后将计算结果缓存起来。 如果 dirty 属性为 false,就直接返回缓存中的结果。

举个例子:

假设我们有以下代码:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    doubleCount: (state) => state.count * 2
  }
})

// 在组件中使用
console.log(store.getters.doubleCount) // 输出 0,并缓存结果
console.log(store.getters.doubleCount) // 输出 0,直接从缓存中获取
store.state.count = 1
console.log(store.getters.doubleCount) // 输出 2,重新计算并缓存结果
console.log(store.getters.doubleCount) // 输出 2,直接从缓存中获取
  • 第一次访问 store.getters.doubleCount 时,doubleCount 对应的计算属性 watcher 的 dirty 属性为 true,因此会重新计算,并将结果 0 缓存起来。
  • 第二次访问 store.getters.doubleCount 时,dirty 属性为 false,因此直接从缓存中获取结果 0。
  • store.state.count 发生变化时,doubleCount 对应的计算属性 watcher 的 dirty 属性会被设置为 true
  • 第三次访问 store.getters.doubleCount 时,dirty 属性为 true,因此会重新计算,并将结果 2 缓存起来。
  • 第四次访问 store.getters.doubleCount 时,dirty 属性为 false,因此直接从缓存中获取结果 2。

总结一下:

机制 描述 作用
依赖收集 当 getter 函数中使用 state 或其他 getters 时,Vue 会自动记录下依赖关系。 确保当依赖的 stategetters 发生变化时,getter 函数能够自动重新计算。
缓存机制 Vue 的计算属性具有缓存机制,只有当依赖的 stategetters 发生变化时,才会清空缓存并重新计算。 避免不必要的计算,提高性能。

三、 源码片段分析(重点)

为了让大家更深刻地理解 getters 的实现,我们来分析一些关键的源码片段。

1. registerGetter 函数

function registerGetter (store, key, getter, module) {
  if (store._wrappedGetters[key]) {
    console.error(`[vuex] duplicate getter key: ${key}`)
    return
  }
  store._wrappedGetters[key] = function wrappedGetter (store) {
    return getter(
      module.namespaced
        ? getNestedState(store.state, module.path)
        : store.state, // local state
      store.getters, // getters
      store.rootState // root state
    )
  }
}
  • store._wrappedGetters[key] = function wrappedGetter (store) { ... } 这里定义了一个 wrappedGetter 函数,并将它存储到 store._wrappedGetters 对象上。 这个 wrappedGetter 函数是实际被 Vue 计算属性调用的函数。
  • getter( ... ) 这里调用了原始的 getter 函数,并传入了 stategettersrootState。 注意这里传入的 state 可能是模块的局部 state,也可能是根 state,取决于模块是否开启了命名空间。
  • module.namespaced ? getNestedState(store.state, module.path) : store.state 这个三元运算符用于判断是否使用模块的局部 state。 如果模块开启了命名空间,就使用 getNestedState 函数获取模块的局部 state;否则,就使用根 state。

2. resetStoreVM 函数

function resetStoreVM (store, state, hot) {
  // ... 其他代码

  const computed = {}
  forEachValue(store.getters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = partial(fn, store)
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  // ...
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })

  // ... 其他代码
}
  • computed[key] = partial(fn, store) 这里创建了一个偏函数,把 store 作为参数预先传入 fn 函数,也就是我们前面包装过的 wrappedGetter 函数。 这样,当这个偏函数被调用时,就会自动传入 store 参数。
  • new Vue({ computed }) 这里创建了一个 Vue 实例,并将 computed 对象作为 computed 属性传入。 这样,Vue 就会自动为每一个 getter 创建一个计算属性。

四、总结与思考

今天我们深入探讨了 Vuex 中 getters 的实现原理、依赖收集和缓存机制。 希望通过这次分析,大家能够对 Vuex 的内部运作有更深入的了解。

一些思考题:

  1. 如果我希望禁用某个 getter 的缓存机制,应该怎么做? (提示:可以考虑使用函数来替代 getter,或者使用 Vue 的 watch 选项。)
  2. 如果我的 getter 函数非常复杂,计算量很大,应该如何优化性能? (提示:可以考虑使用 memoization 技术,或者将计算任务放到 Web Worker 中。)
  3. Vuex 的 getters 和 Vue 组件的 computed 属性有什么异同?

好了,今天的讲座就到这里。 希望大家有所收获,下次再见!

(完)

发表回复

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