【Vue3】day72-reactivity

作者:mario 发布时间: 2026-09-03 阅读量:6 评论数:0

Day 72 · 响应式核心 — ref / reactive / shallowRef 与模板语法

响应式是 Vue 的心脏,也是"命令式 → 声明式"转型的地基。今天用三个实战组件吃透它:标题栏状态灯(连接状态的响应式映射)、屏切换的 v-show(对照 Day 64 的 opacity 策略)、大快照的 shallowRef(性能视角的响应式)。原则:每个 API 都在大屏里找到它的位置,不背概念。


目录


一、ref:默认选择

import { ref } from "vue";

// 任意类型的单一值:基础类型、对象、Map 都可以
const connState = ref<"connected" | "connecting" | "closed">("closed");
const latest = ref<PanelSnapshot | null>(null);

// script 中读写都要 .value;模板中自动解包
connState.value = "connected";

团队规范:默认 ref。理由:

  1. 基础类型只有 ref 能装(reactive 不支持)
  2. 整体替换赋值语义清晰(snapshot.value = newSnap),reactive 整体替换会断开代理
  3. .value 是显式的"我要动响应式状态了"标记,代码审查时一眼可辨

二、reactive 与它的解构陷阱

import { reactive } from "vue";

// reactive:对象的深层代理,适合"结构稳定的组态对象"
const layout = reactive({
  currentScreen: "A",
  autoplay: true,
  dwellSec: { A: 15, B: 20, C: 15 },
});

// ✅ 属性读写直接来,无 .value
layout.autoplay = false;

// ❌ 陷阱一:解构丢响应性——拷贝出了普通值
const { autoplay } = layout;      // autoplay 从此是死数据
// 修复:toRefs 保持响应式连接
const { autoplay } = toRefs(layout);  // 现在是 ref,模板里可用

// ❌ 陷阱二:整体替换断开代理
layout = { ...newLayout };        // 编译报错(const)+即使 let 也丢响应

大屏使用守则:结构固定、整体不变、逐字段更新的"配置/组态"用 reactive(如轮播配置);数据流(快照、告警、窗口)一律 ref——它们是"整体到达、整体替换"的形态。


三、shallowRef:大快照的正确姿势

Day 65 的 Store 每 500ms 收到一次 PanelSnapshot(多设备多字段的大对象)。如果用 ref

const latest = ref<PanelSnapshot | null>(null);
latest.value = newSnap;   // ❌ Vue 会递归遍历整个对象做深层代理
                          //    每秒两次深代理 = 无谓的 GC 压力

大屏快照的特征:从不原地改字段,永远整体替换。这正是 shallowRef 的适用场景:

import { shallowRef, triggerRef } from "vue";

const latest = shallowRef<PanelSnapshot | null>(null);
latest.value = newSnap;   // ✅ 只追踪 .value 的替换,不深入对象内部
                          //    引用变了 → 通知更新,性能开销 O(1)

🎯 选型三连:单一值 → ref;结构稳定的组态 → reactive;大而整体替换的数据(快照/窗口 Map)→ shallowRef。这条纪律直接决定 Day 77 性能对比的成绩单。


四、模板语法速览:大屏用到的 80%

<template>
  <!-- 1. 插值:文本({{ }})与属性(:attr) -->
  <span>{{ connState }}</span>
  <div :class="{ 'is-danger': connState === 'closed' }">●</div>

  <!-- 2. 列表:v-for + key(key 纪律见 Day 74) -->
  <li v-for="alarm in alarms" :key="alarm.id">{{ alarm.text }}</li>

  <!-- 3. 条件渲染:v-if(真移除)与 v-show(display 切换) -->
  <div v-if="alarms.length === 0" class="empty">暂无告警</div>
  <div v-show="isActive">…</div>

  <!-- 4. 事件:@event,内联或方法名 -->
  <button @click="toggleAutoplay">暂停轮播</button>

  <!-- 5. 双向绑定:表单场景才用(大屏少用,意识到位即可) -->
  <input v-model="dwellSec" />
</template>

和原生版的对照v-for = 你的 forEach + createElement + append:class = 你的 classList.togglev-if = remove/append一行模板换十行 DOM 手艺,这就是声明式的兑换率


五、v-if vs v-show:三屏互斥的选型

v-if v-show
实现 真挂载/卸载 DOM display: none 切换
切换成本 高(重建组件树、ECharts 重新 init) 极低
初始成本 低(不渲染) 高(全量渲染)

Day 64 的架构决策(三屏隐藏用 opacity 而非 display/移除,ECharts 隐藏态 init、切屏秒恢复)在 Vue 里的映射就是 v-show

<!-- 三屏互斥:v-show 保证三屏始终挂载(ECharts 实例常驻) -->
<ScreenA v-show="current === 'A'" />
<ScreenB v-show="current === 'B'" />
<ScreenC v-show="current === 'C'" />

<!-- 但需要注意:v-show 用的是 display:none,隐藏容器宽高为 0,
     Day 52 坑 1(隐藏态图表)依然存在 → 切屏后 resize 的纪律照样要守 -->

⚠️ v-show 的 display:none 与 Day 64 的 opacity 策略有差异:display:none 下容器尺寸为 0,首次 init 会失败。两种解法:(1) 首屏渲染完再 init(nextTick 后再 echarts.init);(2) 隐藏类改回 opacity 方案(全局 CSS 覆盖)。Day 76 封装 VChart 时统一解决,今天先知道这个坑的存在。


六、实战:状态灯与标题栏组件

<!-- components/ConnIndicator.vue -->
<script setup lang="ts">
/**
 * 连接状态灯:全屏共享的顶栏组件
 * 迁移自原生版 Day 63 的状态灯——当时是 onState 回调改 classList,
 * 现在是 connState(ref)到 class 的声明式映射
 */
import { computed } from "vue";

// props 由父级传入(Day 74 详讲,今天先用最简形式)
const props = defineProps<{ state: "connected" | "connecting" | "closed" }>();

// 派生状态:文案与配色(computed 详解见 Day 73,这里先感受"状态映射")
const label = computed(
  () => ({ connected: "已连接", connecting: "连接中…", closed: "连接断开" })[props.state]
);
</script>

<template>
  <span class="conn" :data-state="state">
    <i class="conn__dot" />
    {{ label }}
  </span>
</template>

<style scoped>
.conn { display: inline-flex; gap: 6px; align-items: center; font-size: var(--fs-dim); }
.conn__dot { width: 8px; height: 8px; border-radius: 50%; }
/* 状态色全部来自 Day 66 token——视觉零变化 */
.conn[data-state="connected"] .conn__dot { background: var(--state-ok); }
.conn[data-state="connecting"] .conn__dot { background: var(--state-warn); }
.conn[data-state="closed"]    .conn__dot { background: var(--state-danger); }
</style>

使用处(App.vue 标题栏):

<script setup lang="ts">
import { ref } from "vue";
import ConnIndicator from "./components/ConnIndicator.vue";
// 暂时手动驱动状态验证组件,Day 75 接入真实数据源
const connState = ref<"connected" | "connecting" | "closed">("connecting");
setTimeout(() => (connState.value = "connected"), 2000);
</script>

<template>
  <header class="top-bar">
    <h1>智慧工厂监控中心</h1>
    <ConnIndicator :state="connState" />
  </header>
</template>

七、常见坑点

坑 1:reactive 解构后"数据不动了"

从 reactive 对象解构出的变量是普通值,后续对象更新它不跟随。修复:toRefs / toRef,或干脆用 ref。

坑 2:ref 装对象后误以为深层不响应

ref(obj)深层响应的(内部对对象调用 reactive)。想要"只追踪整体替换"必须显式 shallowRef。区分清楚,别在性能敏感处用错。

坑 3:模板里对 ref 写 .value

模板自动解包,写 {{ scale.value }} 会显示数值的字符串包装或报错。记忆口诀:模板不见 value,script 必见 value

坑 4:v-show 隐藏态初始化图表失败

display:none 容器宽高为 0,ECharts init 出 0×0 画布。今天先规避(暂不 init),Day 76 在 VChart 组件里用 nextTick + 激活 resize 系统性解决。


八、自测挑战

T1 · 三类响应式选型练习(30 分钟)

把大屏的六种状态(连接状态 / 最新快照 / 温度窗口 Map / 轮播配置 / 告警数组 / 缩放系数)分别选型 ref / reactive / shallowRef 并写理由。写在 NOTES.md

T2 · 状态灯 + 标题栏组件(60 分钟)

完成第六节组件:三种状态自动轮换(setInterval 每 3s 切换 connected → connecting → closed),观察灯色与文案跟随。然后故意把 connState 改成 reactive 对象的解构变量,验证"数据死了"的坑 1。

T3 · v-show 三屏切换(30 分钟)

App.vue 里加 current ref 和三个切换按钮,用 v-show 互斥显示三屏占位组件。为三屏写全局 .screen 布局类(非 scoped),确认切换无白屏。


九、总结

概念 大屏对应物 纪律
ref 连接状态、告警数组、缩放系数 默认选择,script 里 .value
reactive 轮播配置等结构稳定组态 禁解构(要解构用 toRefs)、禁整体替换
shallowRef 快照、温度窗口 大对象整体替换的唯一正确姿势
v-show 三屏互斥 保 ECharts 常驻;隐藏态尺寸坑 Day 76 解决
:class / :data-state 状态 → 视觉映射 状态色走 token,不写死色值

明天给响应式装上"自动计算与联动"的引擎:computed、watch 与生命周期。

评论