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;

refreactive 的基本区别

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>

pricecount 变化时,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}`);

也可以定义 getset

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

refreactive 没有绝对优劣。保持项目风格一致比机械选择更重要。

十四、本章小结

可以用下面的规则快速判断:

text 复制代码
需要保存会变化的数据       → ref / reactive
需要根据已有数据得到新结果 → computed
需要数据变化后执行操作     → watch

下一章将专门学习 watch,理解 Vue 如何在状态变化后执行请求、同步索引或更新第三方模块。

相关推荐
OpenTiny社区1 小时前
GenUI SDK v1.3.0 发布|多框架兼容,一键换物料,渲染器 & 演练场全面增强!
前端·ai编程
用户938515635071 小时前
useRef + Web Worker 实战:React 如何优雅地拥抱多线程
前端·javascript·react.js
嘟嘟07171 小时前
useRef + Web Worker:React 中的耗时计算不卡页面
javascript·vue.js
mONESY1 小时前
React Router v7 完全实战指南:从入门到鉴权,一套代码全搞定
javascript
万敏1 小时前
Vue3 全栈实战第五周:Vitest 单元测试从零搭建实战记录
vue.js·node.js·全栈
他们叫我秃子1 小时前
前端开发转 Go 全栈(五):终于遇到熟人了,Go 的闭包和高阶函数原来这么像 JavaScript
前端·后端·go
用户61595868000221 小时前
从 0 写一个迷你 wujie:用原生 Web API 实现微前端
前端
妙码生花1 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十七):AI时代对组件封装的新理解、agInput 统一入口,动态表单预备
前端·后端·go
fairyly1 小时前
接手新项目不用硬啃源码,我的 TRAE Work 快速上手实战工作流
前端·ai编程·trae