ref、reactive 与 computed 响应式数据
一、什么是响应式
普通 JavaScript 变量改变后,浏览器页面不会自动跟着变化:
js
let count = 0;
count += 1;
如果页面已经显示了 count,仍然需要手动找到 DOM 并更新内容。
Vue 的响应式系统解决的是:
text
修改状态
↓
Vue 发现状态变化
↓
重新计算相关结果
↓
自动更新页面
在 Vue 3 组合式 API 中,最常见的响应式工具是:
ref();reactive();computed()。
二、使用 ref() 保存状态
ts
import { ref } from 'vue';
const count = ref(0);
修改时使用:
ts
count.value += 1;
为什么需要 .value
ref() 返回的不是原始数字,而是一个包装对象:
ts
{
value: 0
}
Vue 通过这个包装对象追踪读取和修改。
模板中为什么不用 .value
vue
<p>{{ count }}</p>
Vue 模板会自动解包 ref,因此模板中直接写变量名即可。
三、ref() 可以保存哪些内容
1. 基础类型
ts
const loading = ref(false);
const title = ref('实时监控');
const playbackIndex = ref(0);
2. 数组
ts
interface WarningItem {
id: number;
message: string;
}
const warnings = ref<WarningItem[]>([]);
更新数组:
ts
warnings.value.push({ id: 1, message: '温度偏高' });
warnings.value = [];
3. 对象
ts
interface HeatInfo {
heatId: string;
converterNo: string;
}
const heatInfo = ref<HeatInfo | null>(null);
接口返回后赋值:
ts
heatInfo.value = {
heatId: 'H20260805',
converterNo: 'BOF-1'
};
4. DOM 引用
ts
const containerRef = ref<HTMLElement>();
这类 ref 保存的是组件挂载后的 DOM 元素。
四、使用 reactive() 保存对象
ts
import { reactive } from 'vue';
const form = reactive({
heatId: '',
displayMode: 'playback',
autoRefresh: false
});
修改时不需要 .value:
ts
form.heatId = 'H001';
form.autoRefresh = true;
ref 与 reactive 的基本区别
text
ref 可以保存任何类型,脚本中通过 .value 访问
reactive 主要包装对象,直接访问对象属性
初学阶段可以采用简单规则:
- 独立状态优先使用
ref; - 一组紧密相关的表单字段可以使用
reactive; - 项目已经形成统一风格时,优先遵循项目约定。
五、reactive 的解构问题
下面的写法可能破坏响应式联系:
ts
const state = reactive({ count: 0 });
const { count } = state;
此时 count 是普通值。
需要解构时可以使用:
ts
import { toRefs } from 'vue';
const { count } = toRefs(state);
对于初学者,避免随意解构 reactive 对象即可。
六、什么是派生状态
假设页面保存:
ts
const displayMode = ref<'realtime' | 'playback'>('playback');
是否处于实时模式可以由它推导:
ts
const isRealtimeMode = computed(
() => displayMode.value === 'realtime'
);
displayMode 是原始状态,isRealtimeMode 是派生状态。
七、使用 computed()
ts
import { computed, ref } from 'vue';
const price = ref(100);
const count = ref(2);
const total = computed(() => price.value * count.value);
模板中:
vue
<p>总价:{{ total }}</p>
当 price 或 count 变化时,total 自动重新计算。
计算属性具有缓存
只要依赖没有变化,多次读取 total.value 不需要重复执行计算逻辑。
因此,适合把下面内容放到 computed:
- 数据过滤;
- 状态判断;
- 显示文本;
- 多个来源的数据优先级;
- 风险等级;
- 当前帧;
- 列表汇总。
八、从多个数据源选择当前数据
实时页面经常同时具有:
- 实时快照;
- 历史回放;
- 基础详情。
可以使用计算属性统一选择:
ts
const heatMetaView = computed(() => {
if (isRealtimeMode.value) {
return snapshot.value?.heat
|| playback.value?.heat
|| heatMeta.value;
}
return playback.value?.heat || heatMeta.value;
});
这样模板不需要反复判断:
vue
<strong>{{ heatMetaView?.converterNo || '--' }}</strong>
数据链变成:
text
snapshot / playback / heatMeta
↓
heatMetaView
↓
模板
九、根据原始信号计算风险状态
ts
const offgasCo = ref(72);
const warnings = ref<string[]>([]);
const riskClass = computed(() => {
if (warnings.value.includes('critical')) {
return 'critical';
}
if (warnings.value.length > 0 || offgasCo.value >= 80) {
return 'warning';
}
return 'normal';
});
模板中:
vue
<div :class="`risk-${riskClass}`">
当前状态:{{ riskClass }}
</div>
这比在模板中重复编写风险判断更清晰。
十、只读计算属性与可写计算属性
大多数计算属性是只读的:
ts
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
也可以定义 get 和 set:
ts
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value: string) {
const [first, last] = value.split(' ');
firstName.value = first || '';
lastName.value = last || '';
}
});
快速入门阶段,应优先使用只读计算属性。可写计算属性只在确有双向转换需求时使用。
十一、计算属性中不应该做什么
计算属性应该保持"根据输入返回结果"。不建议在其中:
ts
const data = computed(() => {
fetch('/api/data'); // 不推荐
timer = setInterval(...); // 不推荐
count.value += 1; // 不推荐
return count.value;
});
发送请求、启动定时器、修改外部状态属于副作用,应放到函数、生命周期或 watch 中。
十二、一个完整示例
vue
<template>
<section>
<h2>{{ modeTitle }}</h2>
<p>炉次:{{ currentHeat?.heatId || '--' }}</p>
<p>转炉:{{ currentHeat?.converterNo || '--' }}</p>
<p :class="`risk-${riskLevel}`">风险:{{ riskText }}</p>
<button @click="displayMode = 'realtime'">实时</button>
<button @click="displayMode = 'playback'">回放</button>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
type DisplayMode = 'realtime' | 'playback';
interface HeatInfo {
heatId: string;
converterNo: string;
}
const displayMode = ref<DisplayMode>('playback');
const snapshotHeat = ref<HeatInfo | null>(null);
const playbackHeat = ref<HeatInfo | null>({
heatId: 'H001',
converterNo: 'BOF-1'
});
const offgasCo = ref(82);
const isRealtimeMode = computed(
() => displayMode.value === 'realtime'
);
const modeTitle = computed(
() => isRealtimeMode.value ? '实时监控' : '历史回放'
);
const currentHeat = computed(
() => isRealtimeMode.value
? snapshotHeat.value || playbackHeat.value
: playbackHeat.value
);
const riskLevel = computed(() => {
return offgasCo.value >= 80 ? 'warning' : 'normal';
});
const riskText = computed(() => {
return riskLevel.value === 'warning' ? '需要关注' : '正常';
});
</script>
十三、常见误区
1. 忘记 .value
脚本中:
ts
loading.value = true;
模板中:
vue
{{ loading }}
2. 把可以计算的结果重复存成 ref
如果一个值完全由其他状态决定,应优先使用 computed,避免两份状态不同步。
3. 在计算属性中修改依赖
可能导致循环更新或难以追踪的副作用。
4. 把所有对象都改成 reactive
ref 和 reactive 没有绝对优劣。保持项目风格一致比机械选择更重要。
十四、本章小结
可以用下面的规则快速判断:
text
需要保存会变化的数据 → ref / reactive
需要根据已有数据得到新结果 → computed
需要数据变化后执行操作 → watch
下一章将专门学习 watch,理解 Vue 如何在状态变化后执行请求、同步索引或更新第三方模块。