Vue响应式系统中的computed和watch

  • 前面写了两篇文章,分别是Vue双向绑定的原理和实现。里面只是分析了data的响应式原理,但Vue的响应式系统中还有其他的属性。所以本篇讨论一下另外两个常用属性:computedwatch
  • 可以查看前面的博客,对Vue的响应式系统有一定的了解后,再看本篇:

Vue双向绑定:原理篇(详细)

Vue双向绑定:实现篇

一、computed

Vue官方文档—计算属性

1. 基本使用
<template>
	<div>{{b}}</div>
</template>
<script>
export default {
    data: {
        a: 10
    },
    computed: {
        b: function() {
            return this.a * 2 // a发生变化时,重新计算,得到b值
        },
        c() { // 也可以这样写
        	return this.a * 2
        }
    }
 }
</script>
2. 重新计算
  • 计算属性会通过Watcher来观察它所用到的所有属性的变化,当这些属性发生变化时,计算属性会将自身的Watcherdirty属性设置为true,进行重新计算,计算完成后将dirty属性设置为false
3. 缓存
  • 计算属性的特点是有缓存。计算属性函数所依赖的数据在没有发生变化的情况下,反复读取计算属性,而计算属性函数并不会反复执行。
4. 计算结果与重新渲染

旧版,不对比计算结果

  • 当计算属性中的内容发生变化后,计算属性的Watcher与组件Watcher都会得到通知。
  • 组件中的Watcher得到通知,从而执行render函数,所以会重新读取计算属性的值,这时候计算属性Watcher已经把自己的dirty属性设置为true,所以会从重新计算一次计算属性的值,用于本次渲染。

新版,对比计算结果

这是在2.5.2版本中实现的。

  • 组件的Watcher不再观察计算属性用到的数据变化,而是数据变化通知计算属性的Watcher,计算一次计算属性的值,如果发现这一次计算出来的值与上一次计算出来的值不一样,再去主动通知组件的Watcher进行重新渲染操作。
  • 只有计算属性的返回值真的变了,才会重新渲染。
5. 注意命名冲突
  • 计算属性与methods重名,计算属性会悄悄失效
  • data和props才会警告
6. 使用场景
  • 从上面分析可以看出,computed用在一个变量被其他几个变量影响,而且这个变量需要在模板中使用(常见情况)。
  • 举个例子,页面需要显示出总价(总价 = 数量 * 价格),当数量和价格发生变化时都会影响总价,这时计算属性就派上用场了。
  • 另外,computed默认只有getter方法,因为是默认值所以我们也常常省略不写,也就是只能读取,不能改变设值。当赋值给计算属性的时候,将调用setter函数(很少使用)。
7. 总结
  • 简单来说,计算属性相当于定义在变量上的getter方法。当然中间是调用的变量本身的get方法(数据劫持),然后通过Watcher通知计算属性执行。执行结果会进行缓存。下一次执行的时候,会跟上一次的结果进行对比,如果一样,就不会触发重新渲染。
  • 好处
    • 代码维护:computed的设计初衷在避免 template 里面直接计算 {{this.firstName + ’ ’ + this.lastName}}。因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响。
    • 性能优化:当然,因为computed的缓存和对计算结果的比较,避免了重复复杂计算和视图的不必要更新带来的性能浪费。

二、watch

Vue官方文档—侦听器

1. 基本使用
data: {
	a: {
		b: {
			c: 1
		}
	}
}
watch: {
	a.b.c: function(newVal, oldVal){
		// a.b.c发生变化时,调用函数
	}
}
  • 监听的data中的键名,一般为以点分隔的路径,例如a.b.c。

  • 回调函数调用时,会从参数中得到新数据(new value)和旧数据(old value)

回调函数可以写在methods中:

watch: {
   message: 'handler'
 },
methods: {
  handler (newVal, oldVal) { /* ... */ }
}
2. options

deep 深度监听

  • 深度监听watch 监听中一项很重要的配置,它能为我们观察对象中任何一个属性的变化
  • deep 即深入观察, 监听器会层层遍历, 给对象的所有属性(及子属性)添加监听器. 这样做无疑会有很大的性能开销, 修改obj 中任何一个属性都会触发监听器中的处理函数
watch: {
  'obj.hello': {
    handler(newVal, oldVal) {
      console.log(`obj changed: ${newVal}`);
    },
    deep: false
  }
}

immediate 立刻执行

  • 在某些情况下, 你可能需要在创建组件后立即运行监听程序

  • 使用immediate: true 选项, 这样它就会在组件创建时立即执行

3. 动态添加watch
mounted(){
  this.$watch('obj.hello', this.handler, {
    immediate: true,
    deep: false
  })
},
methods: {
  handler(newVal, oldVal) {
    console.log(`obj changed: ${newVal}`);
  }
}

注意:正常情况下, 不推荐使用$watch来动态添加watch, 因为你还需要手动注销watch监听事件

注销watch

  • 若使用动态添加watch, 就需要手动注销了. 从源代码中, 可以看出: this.$watch调用后会返回一个函数, 通过调用这个函数, 即可注销watch。
4. 使用场景
  • watch是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化。比如监听a变量变化后,弹出一个提示框。
5. 与computed的区别
  • 没有缓存,变量变化就会触发,所以就没有性能优化
  • 变量可以一个影响多个
  • 不需要返回值
  • 显式指定依赖源
6. 源码分析

这里分析一下watch初始化,Vue初始化状态的最后一步是初始化watch。

  • 当设置了watch选项并且watch选项不等于浏览器原生的watch时,才开始初始化watch。
if(opts.watch && opts.watch != nativeWatch){
	initWatch(vm, opts.watch)
}
// 因为Firefox浏览器中的Object.prototype上有一个watch方法,避免混淆
  • 循环watch选项,将对象中的每一项依次调用vm.$watch来观察表达式或computedVue.js实例上的变化即可。
  • 由于watch选项的值同时支持字符串、函数、对象和数组类型,不同的类型有不同的用法,所以在调用vm.$watch之前需要对这些类型进行适配。
function initWatch (vm, watch) {
	for (const key in watch) {// 多个watch需要获取每一项的回调函数
		const handler = watch[key]
		if (Array.isArray(handler)) { // 多个回调
			for (let i = 0; i< handler.length; i++) {
				creatWatcher(vm, key, handler[i]) 
			}else{
				createWatcher(vm, key, handler)
			}
		}
	}
}
//for in 循环遍历watch对象,通过key得到watch对象的值并赋值给变量handler
  • createWatcher函数主要负责处理其他类型的handler并调用vm.$watch创建Watcher观察表达式,其代码如下:
function createWatcher (vm, expOrFn, handler, options) {
	if (isPlainObject(handler)) {
		options = handler
		handler = handler.handler
	}
	if (typeof handler === 'string') {
		handler = vm[handler]
	}
	return vm.$watch(expOrFn, handler, options)
}
  • vm.$watch其实是对Watcher的一种封装,Watcher的原理在前面的文章讲过。通过Watcher完全可以实现vm.$watch的功能
Vue.prototype.$watch = function (expOrFn, cb, options) {
	const vm = this
	options = options || {}
	const watcher = new Watcher(vm, expOrFn, cb, options)
	if(options.immediate) {
		cb.call(vm, watcher.value)
	}
	return function unwatchFn() {
		watcher.teardown()
	}
}

可以看到,上面代码是执行new Watcher来实现vm.$watch的基本功能

  • Watcher中,新增了判断expOrFn类型的逻辑。如果expOrFn是函数,且从Vue实例上读取了数据,那么Watcher会观察这几个数据,任意一个变化,Watcher都会得到通知。
  • 如果expOrFn不是函数,则使用parsePath函数来读取Keypath中的数据。这里Keypath指的是属性路径,如 a.b.cWatcher会读取这个Keypath所指向的数据并观察这个数据的变化。

简单总结

  • 在初始化watch的时候,会实例化Watcher,被监听的数据的dep会收集Watcher。
  • 当数据发生变化时,会发布通知,Watcher得到通知后执行回调函数。

三、Vue3中的变化

computed
  • 与Vue2的配置功能一致,在语法有了些变化
  let fullName = computed(()=>{ 
    return person.firstName + "-" + person.lastName; 
  }); 
watch
  • 与Vue2的配置功能一致,在语法有了些变化
watch(
  () => { /* 依赖源收集函数 */ },
  () => { /* 依赖源改变时的回调函数 */ }
)
  setup() {             // reactive 和 ref 都是用来定义响应式数据的
    let obj = reactive({// reactive更推荐去定义复杂的数据类型 ref 更推荐定义基本类型
      name: "张三",
      age: 20,
      friend:{
        name:'李四',
        age:21
      }
    });
    watch(obj, (newValue, oldValue) => {
      console.log("obj改变了", newValue, oldValue);
    },{deep:true});
    return {
      obj,
    };

只监听某个属性

watch(()=>person.age,(newValue,oldValue)=>{ 
  console.log("person的age变化了",newValue,oldValue) 
}) 
新增watchEffect()
  • 可以自动识别回调中使用了哪个数据来对该数据进行监听,有点像计算属性,但是computed注重的是计算产生的值,所以需要写返回值,watchEffect注重数据变化时回调函数的执行,不需要返回值。
watchEffect(
  () => { /* 依赖源同时是回调函数 */ }
)

watchEffect 相当于将 watch 的依赖源和回调函数合并,且初始化时回到函数立刻执行

const counter = ref(1)

watchEffect(
  () => console.log(counter.value)
)
// 相当于
watch(
  () => counter.value,
  () => console.log(counter.value),
  { immediate: true }
)

参考

《深入浅出Vue.js》 ——刘博文

一网打尽──Vue3 Composition-api新特性