Vuex registerModule 里的坑

问题描述

如果你在项目里使用vuex,且存在以下场景:

  1. 组件里通过mapGettersstore中的getter映射到局部计算属性
  2. getter返回的是一引用类型
  3. 组件的watch里对该getter进行监听并做处理
  4. 最重要的是,你使用registerModule异步注册一个动态模块

如果以上条件都满足,那么,恭喜你,第4条中的registerModule每调用一次,第3条中就会处理一次。这也就意味着,每调用一次registerModule,getter返回的值的引用就变化一次,这是为什么呢?

首先,我们通过简单的代码来描述以上所述的场景:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    obj: {
      key: 'value'
    }
  },
  getters: {
    obj(state) {
      return state.obj
    }
  },
  mutations: {
    // ..
  },
  actions: {
    // ...
  },
  modules: {
    childModuleOne: {
      // state、getters、mutations、actions...
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// App.vue
<template>
  <div id="app"></div>
</template>
<script type="text/ecmascript-6">
  import Vue from 'vue'
  import Vuex, { mapGetters } from 'vuex'
  import store from './store'
  export default {
    computed: {
      ...mapGetters([
        'obj'
      ])
    },
    watch: {
      obj(val) {
        console.log(val)
      }
    },
    mounted() {
      store.registerModule('childModuleTwo', {
        // state、getters、mutations、actions...
      })
    }
  }
</script>
<style>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// main.js
import Vue from 'vue'
import Vuex from 'vuex'
import store from './store'
import App from './App.vue'

new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
1
2
3
4
5
6
7
8
9
10
11

问题追踪

学习vuex的源码,我们可以知道,vuexstore.getters是通过新建一 Vue 实例store._vm,通过将store.getters绑定到store._vm的计算属性上,以此来实现getter的响应式处理。

export class Store {
  constructor (options = {}) {
    // ...
  }
  // ...
  registerModule (path, rawModule) {
    if (typeof path === 'string') path = [path]
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    this._modules.register(path, rawModule)
    installModule(this, this.state, path, this._modules.get(path))
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

查看以上registerModule可以看出,在调用installModule进行安装模块之后,会调用resetStoreVM进行重置store._vm,问题就出在resetStoreVM这个函数上。

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] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

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

  // enable strict mode for new vm
  // 开启严格模式
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        // 问题就出在这,$state 的改变,会导致监听 getter 的 watch 重新计算
        oldVm._data.$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

通过分析resetStoreVM的源码,我们知道,在vuex安装了新的子模块之后,需要重置store._vm为一新的 Vue 实例,而老的 Vue 实例oldVm上的数据$state会置为null,此时会触发watch进行重新计算。(watch依赖store._vm.computed.xxx,而store._vm.computed.xxx依赖store._vm._data.$state,因而store._vm._data.$state置为null,会通知store._vm.computed.xxx,导致store._vm.computed.xxx重新计算出新对象,进而store._vm.computed.xxx通知watch,会导致watch重新计算。)

但是,我们不禁好奇,为什么vuex注册新的子模块之后,需要重置store._vm呢?

原因是,新的子模块里也有子模块的getters,这些新的getter也要与store._vm的计算属性进行绑定。但是,我们知道,目前 Vue 还没有提供任何的 API 供我们动态去增加计算属性,因此,只能新建一 Vue 实例来取代旧的store._vm,并将vuex里的所有getter与新的store._vm的计算属性进行一一对应的绑定。