03 ref、reactive 与 computed 响应式数据

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 如何在状态变化后执行请求、同步索引或更新第三方模块。

相关推荐
IT_陈寒6 分钟前
Java项目实战经验分享:一次由JPA懒加载引发的生产事故
前端·人工智能·后端
环境栈笔记9 分钟前
浏览器多开同步怎么做?先分清设置、登录态和窗口操作
前端·人工智能·后端·自动化
是立不是利20 分钟前
CSS 的层叠与优先级——为什么我的样式不生效
前端·css
开开心心就好20 分钟前
电脑语音实时转文字工具,会议记录很省事
运维·服务器·前端·c#·ocr·perl·1024程序员节
aixingpan30 分钟前
aixingpan.cn API开发文档:api_docs_onechart_marks_secondary2接口指南
前端·php
唔661 小时前
在透镜圆正中加了一个描边空心字
前端·javascript
定栓1 小时前
Vite-legacy产物console删除机制
前端·javascript·vue.js
程序员大阳9 小时前
副队长JavaScript教程(49)-主动抛出异常与错误类型
javascript·异常处理·error·throw·错误类型
hhhjhl9 小时前
Protobuf.js 跨端三方库仓颉适配全流程
javascript·网络·udp
康小汪9 小时前
AI 写 Vue 总写成同一个味道:给它装 Skills,装的不是知识是边界
前端·vue.js·人工智能·ai编程·最佳实践