为什么Vuex的state有些需要computed包装?

2,837 阅读2分钟

今天我们来讨论一下为什么有些vuex里的state需要使用computed进行包装,而有些则直接定义在data中就可以使用

问题展示

<body>
    <div id="app">
      <div>{{count}}</div>
      <div>{{d.count}}</div>
      <div>{{$options.render}}</div>
      <button @click="addCount">点击增加</button>
    </div>
</body>

    Vue.use(Vuex);

    const state = {
      count: 1,
    };

    const mutations = {
      addCount(state) {
        console.log("mutation");
        state.count++;
      },
    };

    const actions = {
      addCount({ commit, state }) {
        console.log("action");
        commit("addCount");
      },
    };

    const store = new Vuex.Store({
      state,
      mutations,
      actions,
      /** trict: true*/
    });
    new Vue({
      el: "#app",
      store,
      data() {
        return { count: this.$store.state.count, d: this.$store.state };
      },
      methods: {
        addCount() {
          //   this.d.count++;
          this.$store.state.count++; //没有开启严格模式不会报错
          //   this.$store.dispatch("addCount");
        },
      },
    });

从以上代码来看当我点击按钮时addCount方法会被调用从更新了state中的count的值,这时候页面上会显示什么呢?是一个2还是两个2呢。答案是第一行的<div>{{count}}</div>标签将显示1,而第二行的<div>{{d.count}}</div>将显示2

为什么会出现这样的问题呢?在这里呢我先给大家讲解一下整个页面的初始化依赖初始化过程。

依赖初始化过程

我们知道vue初始化的时候会依次初始化initLifecycleinitEvents...initState等等方法。其中initState中又会初始化initPropsinitMethodsinitData等等。我们着重看一下initData方法

function initData (vm: Component) {
  let data = vm.$options.data
  // 可以当做在这里第一次取值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
   
   // ...省略部分代码
   
  // 对象劫持核心方法 
  observe(data, true /* asRootData */)
}

// 以下是vuex的state响应式的实现
store._vm = new Vue({ data: { ?state: state }, computed })

vuex的响应式实现是直接使用了Vue的响应式机制,在new Vuex.Store()时state就已经完成的响应式的设置,此时state中的所有属性里的Dep对象中的subs属性都是空数组subs: Array<Watcher>; = []。这就意味着此时state中没有任何能够触发的观察者函数。

当代码执行到new Vue({el:'#app'}) 时初始化vue传入的data数据格式为{count: 1, d: this.$store.state},此时this.$store.state.count这段代码将会触发两个get函数分别为statestate.count。但是此时该组件并未挂载渲染所以这两个get函数不会收集到组件的watcher渲染观察者,因为此时Dep.target为undefined。所以count就被赋值为1,再看属性d其指向state的内存所以d被赋值为state的引用

紧接着到了$mount时渲染观察者watcher被创建了,它被赋值到了Dep.target中并调用了render函数。在render中dom被解析成了这样function anonymous( ) { with(this){return _c('div',{attrs:{"id":"app"}},[_c('div',[_v(_s(count))]),_v(" "),_c('div',[_v(_s(d.count))]),_v(" "),_c('div',[_v(_s($options.render))]),_v(" "),_c('button',{on:{"click":addCount}},[_v("点击增加")])])} }, 当执行该方法时又会对countd.count进行取值。 count会收集到当前渲染依赖d也会收集到,那d.count呢?它是指向state.count的所以在取值时会触发statestate.count的依赖收集将渲染观察者收入各自的dep中。所以这就解释了为什么一个能响应一个不能的原因了

Computed响应式

有些同学有疑问为什么我写成这样他就能响应式呢?

new Vue({
      el: "#app",
      store,
      data() {
        return {  d: this.$store.state };
      },
      computed: {
        count() {
            return this.$store.state.count;
        }
      }
      methods: {
        addCount() {
          console.log("addCount");
          //   this.d.count++;
          this.$store.state.count++;
          console.log(this.$store.state.count);
          //   this.$store.dispatch("addCount");
        },
      },
    });

因为 this.$store.state.count 是一个基础数据类型所以在 initDate 时已经变成常量值, 导致后续无法在触发 this.$store.state.count 的依赖搜集。所以需要使用 computed 创建一个watcher 并触发 将渲染watcher收集到 this.$store.state.countdep 中, 这时当修改 this.$store.state.count 值时,会触发计算属性watcherdirty = true, 随后触发渲染watcher获取到最新的值完成渲染

什么是依赖收集

未完待续。。。