Vue响应式系统中的computed和watch
文章目录
- 前面写了两篇文章,分别是Vue双向绑定的原理和实现。里面只是分析了data的响应式原理,但Vue的响应式系统中还有其他的属性。所以本篇讨论一下另外两个常用属性:computed 和 watch。
- 可以查看前面的博客,对Vue的响应式系统有一定的了解后,再看本篇:
一、computed
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
来观察它所用到的所有属性的变化,当这些属性发生变化时,计算属性会将自身的Watcher
的dirty
属性设置为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
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
来观察表达式或computed
在Vue.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.c
。Watcher
会读取这个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》 ——刘博文