阅读 582

为什么Vuex的State需要通过computed引入到视图组件中以完成视图更新响应化?

Motivation

在Vuex的官方文档中,有这么一段话。

Vuex stores are reactive. When Vue components retrieve state from it, they will reactively and efficiently update if the store’s state changes.

Vuex宣称自己的store数据是响应式的。当Vue组件从store里获取到状态时, Vue组件会有效更新视图去反应状态的变化。

我们都知道,在Vue中,我们可以使用computed和watch两种技术监听响应式的data。那么,同理,我们应该可以使用computed和watch来对Vuex的state进行监听和回调。

vue数据和视图绑定

通读Vuex的文档,我们可以发现,文档中只提到了使用computed技术来观察store里的state(或者直接在template里面使用store数据),以此来绑定数据和视图。那么,为什么没有提到watch的方式呢?那我们可不可以使用watch技术来实现数据和视图的绑定呢?可不可以watch vuex store数据呢?怎么watch呢?(可以直接看后面关于watch store数据部分)

这里提到的computed和watch技术包括通过option创建的computed和watch, 还有 composition api 创建的computed和watch。 composition api中关于computed 和 watch方法和option api类似,这里只讲option api。

代码解释

来看看option api: 创建computed watcher和watch watcher是通过initComputed和initWatch来创建的。

首先了解下,为什么使用computed技术,能实现store state和视图的绑定。下面是initComputed的简化版

const computedWatcherOptions = { lazy: true }

function initComputed (vm, computed) {

	// 组件上的computedWatchers对象
  var watchers = vm._computedWatchers = Object.create(null);
 

	// 遍历options中的computed对象
  for (var key in computed) {
	   // 定义在computed中的方法,通过computed中的key值获取
    var userDef = computed[key];
	
    // 赋值给新变量getter
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    
     // 创建wather, 收集依赖,并将Watcher实例放入组建的wather队列里
	   // Watcher构造器接受的入参:
     // vm,expOrFn,cb,options,isRenderWatcher
     watchers[key] = new Watcher(
       vm,
       getter || noop,
       noop,
       computedWatcherOptions
     );
    
     // 用于定义computed属性为响应式。
     defineComputed(vm, key, userDef);    
  }
}

复制代码

这段代码通俗点来讲的话,大概分如下几个步骤:

  1. 为当前组件创建computedWatchers对象
  2. 接着,遍历computed的key,为每一个key创建一个watcher,用于观察computed[key]方法中所‘touch’(vue说法)到的可观察数据。
  3. 定义computed属性(例如vm.a)的getter方法,使其在被‘touch’的时候,能够被当前正在创建的Watcher所依赖(所谓的依赖收集)。

touch数据?

C034A7A9-C014-4247-B26F-D0E82AFBD21F.png

在vue的官方文档中,关于‘Reactivity in Depth’中有这么一幅图,提到了render去touch数据。什么是touch数据? 其实就是调用数据的getter方法。不过这个getter方法已经在defineReactive方法里面被劫持过,拥有被观察的能力。

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // 当被touch时,亦即该property被调用getter时,检查当前是否有全局Watcher
    // 依赖收集逻辑
    if (Dep.target) {
      // 如果存在全局Watcher,那么让此Watcher收集当前data为其依赖
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  }
)
复制代码

回到initComputed来。在该函数中,通过为每个computed的key创建Watcher来touch在computed[key]中被使用的data(observed data)。 在我们的分析中,就是touch在store里定义的数据。 这样,当store中的数据被修改后,watcher就会接收到通知。 不过,由于computedWatcherOptions = {lazy: true}, 所创建的computed watcher是惰性watcher,即,当watcher所观察的依赖发生变化时,并不是立即执行watcher的callback,而是将watcher标记为dirty,当然,computed watcher是没有callback的(其实有一个noop空函数)。

到此为止, 当store的数据发生变化时,是不会更新视图的。我们需要使这个computed属性也是可以被touch的。这就有了defineComputed方法。

defineComputed方法的会为每一个computed属性定义一个新的getter,该getter使其具备被touch的能力,即在mount的时候,被touch,且被更新视图的Watcher依赖上。

下面的代码是computed属性被定义上的新的getter。当被touch(get)时,检查其computed watcher是否dirty,就是检查该computed所观察的数据是否发生过改变(我们前面提到过,computed watcher是惰性watcher)。若果发生改变的话,重新求值。 接下来,检查global的watcher,检查当前是否在发生依赖收集,如果是的话,让这个watcher依赖上我们的computed 属性(视图更新Watcher就是这是依赖上computed属性的)。最后返回watcher的值。

// 获取对应的watcher
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
  // 检查watcher是否有变化,有变化的话,会调用watcher上面的evaluate来获取新的值
  if (watcher.dirty) {
    watcher.evaluate()
  }
  // 检查当前是否有watcher在收集依赖
  if (Dep.target) {
    watcher.depend()
  }
  // 这个返回值就是所谓的cache上的computed值(在watcher不是dirty的情况下)
  return watcher.value
}

复制代码

使用watch来观察store的数据?

那我们是否可以使用watch的方式来绑定vuex数据和视图呢?答案是肯定的,不过需要不一样的技术。先看看initWatcher

for (const key in watch) {
  const handler = watch[key]
  if (Array.isArray(handler)) {
    for (let i = 0; i < handler.length; i++) {
      createWatcher(vm, key, handler[i])
    }
  } else {
    createWatcher(vm, key, handler)
  }
}
复制代码

在initWatch里面,首先遍历option里的watch对象,然后为watch对象的每一个key值创建一个或多个watcher(当watch[key]的value为一个数组。数组的元素可以是组件method的名字,也可以是包含handler的object,也可以是函数)。这里使用了createWatcher方法,createWatcher内部逻辑如下:

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

复制代码

createWatcher封装了一些入参的判断逻辑,最后,调用了vue的$watch方法来观察expOrFn的变化。这里的expOrFn的入参类型为string, 即为watch的key。

到这里,我们可以发现computed的初始化和watch的初始化的最大不同,就是这里的expOrFn和handler.

computed的expOrFn传入的是computed[key](为function),handler为noop。 watch的expOrFn则传入的是watch的key (string ), handler为watch[key]。

本质区别

这就造成了computed和watch的本质区别。

所以,如果我们有如下代码:

computed: {
	a: () => { store.state.count++ }
},
watch: {
	b: () => { this.a++ }
}
复制代码

computed watcher 观察的是store.state.count的变化(touch了store.state.count),而watch watcher观察的是b的变化。

computed观察的是computed[key]函数中调用过getter的可观察数据, 所以在computed[key]中调用了多个可观察数据的getter的话,就可以观察多个数据。而watch观察的是唯一的key, 所以只能进行唯一数据观察。同时,computed没有handler,所以当computed的可观察数据发生变化时,computed的watcher没有调用回调函数的操作,同时由于设置了lazy属性,所以只是标记自己(watcher)为dirty。而wach的可观察数据发生变化时,将会调用handler (watch[key])。

他们的关系大概如下:

Vue Watch Diagram.jpg

watch在store中的数据

那如果我们想直接watch在vuex store中的数据呢,同时又不产生不必要的computed属性的话呢?如下图的红线标示。

83D958E0-3C7E-4D21-A01C-5B82A3A82ECB.png

如果使用option api的watch, 可以这样做: 不过这样watch vuex store中的data是不会更新视图的。

watch: {
  '$store.state.count': function (newval) {
    console.log(newval);
  }
},
复制代码

我们同时可以利用$watch来做到。 当然,使用这种方法,也是不会更新视图的。

created() {
    // top-level property name
    this.$watch('a', (newVal, oldVal) => {
      // do something
    })

    // function for watching a single nested property
    this.$watch(
      () => this.c.d,
      (newVal, oldVal) => {
        // do something
      }
    )

    // function for watching a complex expression
    this.$watch(
      // every time the expression `this.$store.state.a + this.$store.state.b` yields a different result,
      // the handler will be called. It's as if we were watching a computed
      // property without defining the computed property itself
      () => this.$store.state.a + this.$store.state.b,
      (newVal, oldVal) => {
        // do something
      }
    )
  }
复制代码

为了更新视图,你可以在handler里面对一个可观察数据进行赋值,然后再在视图层使用这个值,就能在回调handler以后,更新视图啦。

总结

综上所述,我们知道,如果想追踪store中的数据,并及时更新视图的最简单方法就是使用computed技术来观察数值变化。同时,我们还了解到在组件中使用watch技术来追踪store中的数据来触发回调函数。 我们还在文中通过查看源代码探讨了computed和watch技术的异同点,总结了在组件中观察vuex数据的不同方法。

文章分类
前端
文章标签