前言
网上的给出的区别有很多,今天只是简单来回归下其中的一些流程和区别的关键点。
以下面的代码为例进行思考:
xml
<template>
<div class="calc">
<input v-model.number="n1" type="number" placeholder="数字1">
<input v-model.number="n2" type="number" placeholder="数字2">
<p>和:{{ sum }} 平均值:{{ avg }}</p>
<input v-model="name" placeholder="用户名">
<p :style="{color: tipColor}">{{ tip }}</p>
</div>
</template>
<script>
export default {
data() {
return { n1: 0, n2: 0, name: '', tip: '', tipColor: '#333' }
},
computed: {
sum() { return this.n1 + this.n2 },
avg() { return this.sum / 2 }
},
watch: {
name(v) {
if (!v.trim()) { this.tip = '不能为空'; this.tipColor = 'red' }
else if (v.length < 3) { this.tip = '不少于3位'; this.tipColor = 'orange' }
else { this.tip = '可用'; this.tipColor = 'green' }
}
}
}
</script>
<style scoped>
.calc { padding: 20px; border: 1px solid #eee; width: 300px; margin: 20px auto; }
input { display: block; width: 100%; margin: 8px 0; padding: 6px; border: 1px solid #ccc; border-radius: 4px; }
</style>
关于 watch 和 computed 的使用我们很多,这里我们不一一介绍,但是请记住:监听是不需要 return 的,计算属性是百分百必须要有的。
1、监听和计算属性的区别
最关键的区别:
1、监听是没有缓存的,计算属性是有缓存的
2、监听是不需要有返回值的,但是机损属性是必须要有返回值的。(极限情况下不return基本没意义)
其他的区别:
| 对比维度 | 计算属性(computed) | 监听器(watch) |
|---|---|---|
| 核心用途 | 基于已有数据推导 / 计算新数据(数据转换 / 组合) | 监听已有数据的变化,执行异步操作或复杂逻辑(无新数据产出,侧重 "副作用") |
| 依赖关系 | 只能依赖 Vue 实例中的响应式数据(data/props/ 其他 computed),自动感知依赖变化 |
可监听单个响应式数据、对象属性、数组,甚至通过 deep: true监听对象深层变化,支持手动指定监听目标 |
| 使用场景 | 1. 简单数据拼接(如全名:firstName + lastName)2. 数据格式化(如时间戳转日期字符串)3. 依赖多数据的计算(如总价:price * count)4. 需缓存的重复计算场景 |
1. 异步操作(如监听输入框变化,延迟请求接口获取联想数据)2. 复杂逻辑处理(如监听用户状态变化,同步更新权限菜单)3. 监听对象深层变化(如监听表单对象,统一处理提交前校验)4. 数据变化后的联动操作(非数据推导类) |
| 是否支持异步 | 不支持异步操作:若在 computed中使用异步(如定时器、接口请求),无法正确返回推导结果,会得到 undefined |
支持异步操作:这是 watch的核心优势之一,可在监听函数中执行任意异步逻辑 |
2: 监听和计算属性的基本触发流程:
核心逻辑:set → Dep → Watcher → watch/computed 联动流程
无论是 watch 还是 computed,底层联动流程的核心一致,仅在 Watcher 执行逻辑上有差异,完整流程如下:
第一步:初始化阶段 ------ 依赖收集(get 拦截器 + Dep + Watcher 绑定)
computed****的依赖收集:
- 组件初始化时,会为每个计算属性创建一个「计算属性
Watcher」; 1. 执行计算属性的get方法,访问依赖的响应式数据(如this.num1); 1. 触发该数据的get拦截器,get拦截器会将当前「计算属性Watcher」添加到该数据的Dep依赖列表中; 1. 所有依赖数据都完成Watcher绑定,最终缓存计算属性的初始结果。
watch****的依赖收集:
-
- 组件初始化时,会为每个
watch监听目标创建一个「普通Watcher」; - 主动读取一次监听目标数据(如
this.username),触发该数据的get拦截器; get拦截器将当前「普通Watcher」添加到该数据的Dep依赖列表中;- 若开启
deep: true,会递归遍历对象 / 数组的内部属性,完成深层依赖收集。
- 组件初始化时,会为每个
第二步:更新阶段 ------set 拦截器触发 Watcher 执行
当修改响应式数据时(如 this.num1 = 10),触发底层联动:
- 数据被修改,触发该数据的
set拦截器; set拦截器调用对应Dep的notify()方法(派发更新通知);Dep遍历自身的依赖列表,通知所有绑定的Watcher「数据已更新」;- 不同类型的
Watcher接收通知后,执行差异化操作(这是watch和computed表现不同的核心原因):
-
- 普通
Watcher(对应watch) :收到通知后,立即执行watch的回调函数 ,传入newVal和oldVal,执行异步 / 复杂逻辑; - 计算属性
Watcher(对应computed) :收到通知后,仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行get方法计算新结果并更新缓存。
- 普通
举个例子:
javascript
watch: {
num(newVal, oldVal) {
console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
}
},
当 set 触发 watcher 后,watcher 就会立即触发:
javascript
num(newVal, oldVal) {
console.log(`【watch】:num从${oldVal}变为${newVal},我立即执行回调`);
}
什么叫 收到通知后, 仅将自身标记为「脏状态」(缓存失效) ,不立即执行计算逻辑,等待下次访问计算属性时,才重新执行 get 方法计算新结果并更新缓存。
举个例子:
xml
<template>
<div>
<!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
<p v-if="num < 2">两倍数:{{ numDouble }}</p>
</div>
</template>
<script>
export default {
data() {
return { num: 1 };
},
computed: {
numDouble() {
console.log("计算属性执行计算");
return this.num * 2;
},
},
mounted() {
setTimeout(() => {
this.num = 5; // 2秒后,v-if不成立
}, 2000);
setTimeout(() => {
this.num = 1; // 4秒后,v-if再次成立
}, 4000);
},
};
</script>
// 控制台只会打印2次"计算属性执行
为什么只打印 2 次:
原因就是我们的计算属性在 2s 后没有执行
1、 当初始化时,页面中 v-if 条件是符合的,会执行一次 get 计算得到返回值
2、当经过两秒后、 v-if 不符合条件,这个时候表明numDouble 是脏数据,会对其进行标记( v-if 不符合条件,所以无法对numDouble 进行访问, 这里也就是我们说的缓存,缓存计算属性的结果值,当脏状态取消时才会进行新的计算 )
3、当 经过 4 秒后条件再次被满足时,才会有新的计算。
3: 计算属性为什么不能异步
举个例子,我们使用延时进行模仿:
xml
<template>
<div>
<!-- 这里就是「访问计算属性」:模板渲染时会读取 numDouble 的值 -->
<p v-if="num < 2">两倍数:{{ numDouble }}</p>
</div>
</template>
<script>
export default {
data() {
return { num: 1 };
},
computed: {
numDouble() {
setTimeout(() => {
return this.num * 2;
}, 1000);
},
},
mounted() {
console.log(this.numDouble);
},
};
</script>
打印结果如下:

为什么会打印 underfined 呢,这里有两个原因?
原因 1:JavaScript 中,任何函数如果没有显式写 return 语句,或 return 后没有跟随具体值,都会默认返回 undefined,这是计算属性返回 undefined 的基础原因。
举个例子:
javascript
function test() {
setTimeout(() => {
return 11;
}, 1000);
}
setTimeout(() => {
console.log(test());
}, 2000);
这样写是属于语法的错误。
原因 2:setTimeout 的异步特性(关键)
即使你在 setTimeout 回调中写了 return this.num * 2,这个返回值也毫无意义,因为 setTimeout是异步宏任务:
- 当 Vue 访问
this.numDouble时,会立即执行计算属性的函数体; - 函数体执行到
setTimeout时,只会「注册一个异步任务」,然后直接跳过setTimeout继续执行; - 此时计算属性函数体已经执行完毕(没有显式
return),默认返回undefined,并被console.log打印; - 1 秒后,
setTimeout的回调函数才会执行,此时回调中的return this.num * 2只是回调函数自身的返回值,无法传递给计算属性,也无法改变之前已经返回的undefined。
简单说:异步回调的返回值,无法成为计算属性的返回值,计算属性会在异步任务注册后,直接默认返回 undefined 。
也可以分两步走
一、先明确:计算属性的函数体,只在 "被访问" 时同步执行一次(除非满足重新计算条件)
当 mounted 中访问 this.numDouble,或者模板渲染访问 numDouble 时,Vue 会同步、完整地执行一遍 ****numDouble ****函数体的代码 ,但这个执行过程和 setTimeout 内部的回调是完全分离的:
第一步:计算属性函数体「同步执行」(瞬间完成,不等待异步)
我们把 numDouble 的执行过程拆解成 "逐行执行",你就能看清流程:
javascript
numDouble() {
// 第1步:执行 setTimeout 这行代码
// 作用:向浏览器"注册一个1秒后执行的异步任务",仅此而已
// 注意:这行代码执行时,不会等待1秒,也不会执行回调函数内部的代码
setTimeout(() => {
// 这是回调函数,此时完全没有执行!
console.log("回调函数开始执行");
return this.num * 2;
}, 1000);
// 第2步:计算属性函数体执行到末尾
// 没有显式 return,默认返回 undefined
// 此时,numDouble 已经完成了"返回值"的传递,整个函数体执行结束
}
简单来说就是numDouble 函数体的执行,只做了一件事 ------"安排了一个 1 秒后的任务",然后就直接返回了 undefined,它不会停下来等 1 秒后回调执行完再返回值。
第二步:1 秒后,异步回调才执行,但为时已晚
- 计算属性已经在第一步就返回了
undefined,这个返回值已经被console.log打印,也被 Vue 缓存起来了; - 1 秒后,浏览器才会执行
setTimeout的回调函数,此时回调里的return this.num * 2只是 "回调函数自己的返回值"------ 这个值没有任何接收者,既不能传给numDouble,也不能改变之前已经返回的undefined; - 更关键的是:回调执行时,
numDouble函数体早就执行完毕了,两者是完全独立的执行流程,回调的返回值无法 "回溯" 给已经执行完的计算属性。
简单来讲就是:
计算属性是要内置返回一个结果的,如果加入异步就会因为执行顺讯返回一个undefined,监听是在事件触发后对写入的回调函数的调用。