Skip to content

07|内置指令工具箱:每把扳手都有尺寸,别全拿来敲钉子 ​

第一章概览 / 07。目标:认识常用与少见内置指令各自影响的是内容、事件、渲染、编译还是更新策略。

本页示例运行在 Vue 3.5.x + Vite + TypeScript。内容指令可放进 src/App.vue 直接观察;v-memo 的练习还要打开 Vue DevTools 或浏览器 Performance,固定 500 行和同一组选中操作后再比较。

一张总览图:指令到底动了哪里 ​

分类指令作用层一句话边界
内容与绑定v-text、v-html、v-bind、v-modelDOM 内容/属性数据如何落到元素上
控制流v-if、v-else-if、v-else、v-show、v-forVNode 创建与显示哪些节点存在、怎样重复
交互v-on、v-slot事件/组件内容协议动作如何进来,内容如何分发
编译与更新v-pre、v-once、v-memo、v-cloak编译期或渲染优化少编译、少更新、避免闪烁

前六项在前面几页各有专讲;本页聚焦容易被遗忘的“优化与边界”成员。它们很有用,但都不是性能喷雾剂。

v-text 与 v-html:内容的两条通道 ​

vue
<script setup lang="ts">
const plainNotice = '<strong>这只是文本</strong>'
const trustedHtml = '<strong>这来自受控、已净化的 CMS</strong>'
</script>

<template>
  <p v-text="plainNotice"></p>
  <div v-html="trustedHtml"></div>
</template>

v-text 设置文本内容,安全且常用于想明确排除子节点的场合;普通插值通常更自然。v-html 设置 HTML,必须由可信、已净化的来源提供,且其中内容不会按 Vue 模板再次编译。把组件模板、样式或业务逻辑塞进 v-html,等于请一位不受监管的临时演员上舞台。

内容通道的责任要写在名字里 ​

v-html 不是一种“更强的插值”,而是一次信任升级。只要页面要渲染 HTML,就应能回答:内容来自哪里、在哪一层净化、允许哪些标签和属性、谁负责测试这条边界。变量名也应该暴露这个事实,例如 sanitizedArticleHtml 比 content 更能提醒后来者别把任意用户输入塞进去。

vue
<script setup lang="ts">
const props = defineProps<{
  // 约定:该值已由服务端白名单净化,组件不接收原始用户 HTML。
  sanitizedArticleHtml: string
}>()
</script>

<template>
  <article class="article-body" v-html="props.sanitizedArticleHtml"></article>
</template>

若内容其实是标题、评论、搜索词、错误信息或几段结构固定的数据,默认插值、组件和插槽往往更好:它们可类型化、可测试、能继承组件边界,也不会把安全策略藏在一个 div 里。

v-once:我只演第一场 ​

v-once 让元素及其子树只渲染一次,后续状态变化不再更新这段区域。适合真正的只读静态快照,例如确认提交时记录的订单摘要;不适合会随语言、主题、权限或数据刷新而变化的界面。

vue
<script setup lang="ts">
import { ref } from 'vue'
const submittedAt = ref(new Date().toLocaleString('zh-CN'))
const liveClock = ref(new Date().toLocaleTimeString('zh-CN'))
function tick() {
  liveClock.value = new Date().toLocaleTimeString('zh-CN')
}
</script>

<template>
  <p v-once>这个提交时间在首次渲染后固定:{{ submittedAt }}</p>
  <p>这个时钟仍会更新:{{ liveClock }}</p>
  <button type="button" @click="tick">更新时间</button>
</template>

若一段内容“现在不变、未来也必须不变”,v-once 表达得很准确;若只是你还没决定怎么更新它,先别冻住它。

v-memo:只在依赖变了时重看这棵子树 ​

v-memo 接受一个固定长度的依赖数组。数组中每一项与上次渲染相同,则 Vue 跳过该子树及其后代的 VNode 更新。它面向已测出的热点,例如很大的可选择列表;在普通页面提前使用,只会给维护者添一张谜题卡。

vue
<script setup lang="ts">
import { ref } from 'vue'

type Row = { id: string; name: string; score: number }
const rows = ref<Row[]>([])
const selectedId = ref<string | null>(null)
</script>

<template>
  <ul>
    <li
      v-for="row in rows"
      :key="row.id"
      v-memo="[row.id === selectedId, row.name, row.score]"
      :class="{ selected: row.id === selectedId }"
      @click="selectedId = row.id"
    >
      {{ row.name }}:{{ row.score }}
    </li>
  </ul>
</template>

使用 v-memo 前的四个问题 ​

  1. 是否已经用 Vue DevTools / 浏览器性能面板证明确有更新热点?
  2. 是否先让 props、key、列表虚拟化和组件边界正确?
  3. 依赖数组是否覆盖了子树显示所需的所有变化?遗漏后会得到“界面为什么不刷新”的悬疑片。
  4. 能否让下一位维护者一眼看懂?不能就加注释,或先不加。

v-pre 与 v-cloak:一个跳编译,一个遮首屏 ​

html
<!-- 原样展示模板语法;内部插值、指令都不再由 Vue 处理。 -->
<pre v-pre>{{ message }}</pre>

<!-- 主要用于非构建/CDN 挂载时,避免短暂露出原始插值。 -->
<div v-cloak>{{ message }}</div>
<style>[v-cloak] { display: none; }</style>

现代 Vite 项目通常由打包和挂载流程处理好首屏,不常需要 v-cloak;它仍是阅读旧项目、做渐进接入时该认识的工具。v-pre 则是明确放弃这一片的 Vue 编译,不要误以为它能选择性保留某一个绑定。

深水区:优化指令只在“证据链”最后一环出现 ​

当页面变慢时,先不要把 v-once 与 v-memo 当创可贴。先确认问题是网络、计算、组件更新、巨量 DOM,还是浏览器布局;不同问题没有同一把扳手。下面的路径比“加了以后感觉好一点”更可靠:

  1. 复现。 固定数据量与用户操作,例如连续选择 500 行中的不同项。
  2. 测量。 用 Vue DevTools 或浏览器 Performance 看到哪一层更新、耗时和次数异常。
  3. 先修模型。 稳定 props、正确 key、避免模板副作用、减少渲染数量;列表很大时优先考虑虚拟化。
  4. 最后局部优化。 确认某棵子树只依赖少量值后,才写 v-memo,并把依赖覆盖理由留在注释或验收里。

v-memo 的依赖数组是正确性的合同,不是性能参数。数组漏项,得到的是过期 UI;数组填入每个都会变的对象,又几乎没有跳过更新的收益。优化代码必须同时说明为什么这里可以不更新。

选指令前先回答“我要改变哪一层” ​

你真正要的结果先选什么不该误用成
让一块内容显示纯文本插值或 v-textv-html,除非已有可信净化边界
让节点按状态创建或销毁v-if / 条件分支v-show,若关闭时需要释放内部状态
让已存在节点频繁显隐v-showv-if,若每次切换都不该重建
让列表逐项生成v-for + 稳定 keyv-memo,它不能提供身份
固定真正不变的输出v-once“现在懒得处理更新”的冻结按钮
降低确认过的更新热点v-memo未测量页面的通用性能开关
展示 Vue 语法本身v-pre需要执行绑定的业务区域

这张表有意从“结果”而不是“语法名”出发。Vue 指令是给渲染器的声明,不是一个个可以随意叠加的效果贴纸;先选层级,组合才会有理由。

指令优先级与组合原则 ​

  • 一个元素通常只承担一个主要控制流角色;不要在同一个元素上混用 v-if 与 v-for。
  • v-for 项必须有稳定 :key;v-memo 不能代替 key。
  • v-show 是 CSS 显示策略;v-once / v-memo 是更新策略;v-pre 是编译策略。它们解决的问题根本不同。
  • 指令不能替代业务模型。页面变慢先定位渲染原因,而不是在每个标签上撒 v-memo 胡椒粉。

排错路线:这段 UI 为什么没有按预期刷新 ​

  1. 看它是否被刻意冻结。 v-once 和不完整的 v-memo 都会让新状态到不了屏幕;先移除优化指令验证数据链路。
  2. 看是否跳过了编译。 v-pre 里的插值和指令本来就不会执行;它适合展示语法,不适合装真实交互。
  3. 看内容通道。 v-html 写入的字符串不会再编译为 Vue 模板,也不自动获得 scoped CSS 规则。
  4. 回到状态与渲染。 确认响应式值真在变化,模板确实读取它,再决定问题属于数据、依赖数组还是 DOM 树。

翻车博物馆 ​

症状根因正解
文案/主题切换后某块不变误用 v-once移除它,或只固定真正的快照内容
v-memo 后某字段不刷新依赖数组漏了该字段列出所有影响子树可见输出的依赖
v-html 内的 {{ name }} 没生效插入 HTML 不会再次作为 Vue 模板编译用组件/插槽/结构化渲染替代
v-cloak 永久隐藏Vue 没有成功挂载或 CSS 范围错误先修复挂载错误;不要把 v-cloak 当加载器

练习:把一份昂贵仪表盘优化得有证据 ​

在 src/App.vue 创建 500 行数据,每项合同为 { id: string; name: string; score: number }。页面提供“选择第 250 行”“给第 250 行加 1 分”和“刷新报表时间”三个动作。

先不写 v-memo,固定执行十次选择并记录一次性能轨迹;只有轨迹显示无关行重复更新时才加。为导出时间使用 v-once:点击刷新后实时区域要变,快照区域必须不变。随后故意从 memo 依赖中删掉 row.score 并加分,复现旧分数,再把它补回。

参考答案:查看第一章练习答案中的第 07 题。

验收清单:如何判断练习完成 ​

  • 保留优化前后的同一组操作与数据量;若当前机器看不出有意义差异,应删除 v-memo 并记录“没有证据”,而不是宣称优化成功。
  • 选择与加分都正确显示;漏掉 row.score 时能明确复现陈旧 UI,补回后恢复。
  • v-once 快照不随刷新动作变化,旁边的实时区域会变化;两者的差异可直接观察。

通关自检:你能说清每个少见指令“省掉的是编译、创建还是更新”,就不会把扳手带错工位。

用心记录每一步成长