07|内置指令工具箱:每把扳手都有尺寸,别全拿来敲钉子
第一章概览 / 07。目标:认识常用与少见内置指令各自影响的是内容、事件、渲染、编译还是更新策略。
本页示例运行在 Vue 3.5.x + Vite + TypeScript。内容指令可放进 src/App.vue 直接观察;v-memo 的练习还要打开 Vue DevTools 或浏览器 Performance,固定 500 行和同一组选中操作后再比较。
一张总览图:指令到底动了哪里
| 分类 | 指令 | 作用层 | 一句话边界 |
|---|---|---|---|
| 内容与绑定 | v-text、v-html、v-bind、v-model | DOM 内容/属性 | 数据如何落到元素上 |
| 控制流 | v-if、v-else-if、v-else、v-show、v-for | VNode 创建与显示 | 哪些节点存在、怎样重复 |
| 交互 | v-on、v-slot | 事件/组件内容协议 | 动作如何进来,内容如何分发 |
| 编译与更新 | v-pre、v-once、v-memo、v-cloak | 编译期或渲染优化 | 少编译、少更新、避免闪烁 |
前六项在前面几页各有专讲;本页聚焦容易被遗忘的“优化与边界”成员。它们很有用,但都不是性能喷雾剂。
v-text 与 v-html:内容的两条通道
<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 更能提醒后来者别把任意用户输入塞进去。
<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 让元素及其子树只渲染一次,后续状态变化不再更新这段区域。适合真正的只读静态快照,例如确认提交时记录的订单摘要;不适合会随语言、主题、权限或数据刷新而变化的界面。
<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 更新。它面向已测出的热点,例如很大的可选择列表;在普通页面提前使用,只会给维护者添一张谜题卡。
<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 前的四个问题
- 是否已经用 Vue DevTools / 浏览器性能面板证明确有更新热点?
- 是否先让 props、key、列表虚拟化和组件边界正确?
- 依赖数组是否覆盖了子树显示所需的所有变化?遗漏后会得到“界面为什么不刷新”的悬疑片。
- 能否让下一位维护者一眼看懂?不能就加注释,或先不加。
v-pre 与 v-cloak:一个跳编译,一个遮首屏
<!-- 原样展示模板语法;内部插值、指令都不再由 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,还是浏览器布局;不同问题没有同一把扳手。下面的路径比“加了以后感觉好一点”更可靠:
- 复现。 固定数据量与用户操作,例如连续选择 500 行中的不同项。
- 测量。 用 Vue DevTools 或浏览器 Performance 看到哪一层更新、耗时和次数异常。
- 先修模型。 稳定 props、正确 key、避免模板副作用、减少渲染数量;列表很大时优先考虑虚拟化。
- 最后局部优化。 确认某棵子树只依赖少量值后,才写
v-memo,并把依赖覆盖理由留在注释或验收里。
v-memo 的依赖数组是正确性的合同,不是性能参数。数组漏项,得到的是过期 UI;数组填入每个都会变的对象,又几乎没有跳过更新的收益。优化代码必须同时说明为什么这里可以不更新。
选指令前先回答“我要改变哪一层”
| 你真正要的结果 | 先选什么 | 不该误用成 |
|---|---|---|
| 让一块内容显示纯文本 | 插值或 v-text | v-html,除非已有可信净化边界 |
| 让节点按状态创建或销毁 | v-if / 条件分支 | v-show,若关闭时需要释放内部状态 |
| 让已存在节点频繁显隐 | v-show | v-if,若每次切换都不该重建 |
| 让列表逐项生成 | v-for + 稳定 key | v-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 为什么没有按预期刷新
- 看它是否被刻意冻结。
v-once和不完整的v-memo都会让新状态到不了屏幕;先移除优化指令验证数据链路。 - 看是否跳过了编译。
v-pre里的插值和指令本来就不会执行;它适合展示语法,不适合装真实交互。 - 看内容通道。
v-html写入的字符串不会再编译为 Vue 模板,也不自动获得 scoped CSS 规则。 - 回到状态与渲染。 确认响应式值真在变化,模板确实读取它,再决定问题属于数据、依赖数组还是 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快照不随刷新动作变化,旁边的实时区域会变化;两者的差异可直接观察。
通关自检:你能说清每个少见指令“省掉的是编译、创建还是更新”,就不会把扳手带错工位。