03 · 模板与渲染(Template & Rendering)

📅 预计 75 分钟 | ⭐ = 高频考点 | 📌 中英术语见文末
✍️ 配套练习:网页练习 shengxia.dev/quiz?module=mv03


1.0 先给直觉:模板 = 带空格的简历表

想象一张空白简历表:姓名、学校、技能这些格子是固定的,你只需要往里面填数据。填"小张",表格的"姓名"处就显示小张;换填"小李",自动变成小李。表格本身不变,变的只是数据。

Vue 的**模板(template)**就是这张表,**插值(interpolation)**就是那些"待填格子",数据一变,格子里的内容自动更新。你负责改数据,模板负责"填进去并保持同步"。

💡 记忆口诀:模板定框架,数据来填充,一变自动更。


1.1 插值语法:往格子里填数据 ⭐

双大括号 {{ }}

最基础的填法是把数据用 {{ }} 包起来,位置放哪都行,Vue 会把它替换成数据值:

<template>
  <p>你好,{{ name }}!</p>              <!-- 输出 name 的值 -->
  <p>你今年 {{ age }} 岁。</p>
  <p>1 + 1 = {{ 1 + 1 }}</p>            <!-- 里面可以写简单表达式 -->
  <p>{{ message.toUpperCase() }}</p>    <!-- 甚至可以调用方法 -->
</template>

<script>
export default {
  data() {
    return {
      name: '小张',
      age: 20,
      message: 'hello',
    };
  },
};
</script>

渲染结果:你好,小张!你今年 20 岁。1 + 1 = 2HELLO

要点:

  • {{ }} 里可以放单个表达式:变量、运算、调用方法都可以。
  • 不能放完整的语句(如 if 判断、for 循环),表达式只求值、不执行逻辑。
  • 数据变化时,对应插值自动重新渲染

v-text 与 v-html

{{ }} 的替代写法,以及需要渲染 HTML 时的选择:

<p v-text="content"></p>          <!-- 等价于 {{ content }},插入纯文本 -->
<p v-html="htmlContent"></p>      <!-- 把字符串当 HTML 解析后插入 -->

注意v-html 会真正执行 HTML 标签。如果内容来自用户输入,可能被注入恶意代码(XSS 攻击)。来自用户的内容绝不用 v-html,只对可信的静态 HTML 使用。

⚠️ 常见错误

  1. {{ }} 里写语句{{ if (x) { ... } }} 直接报错或显示异常,只能写表达式。
  2. 变量名拼错{{ nam }} 而 data 里是 name,渲染为空白,不报错,很难发现。
  3. 滥用 v-html:把用户评论用 v-html 渲染,等于把安全问题留给自己。
  4. {{ }} 里做复杂计算{{ list.filter(...).map(...) }} 能跑但不清晰,应该用计算属性(后续讲义)。

1.2 指令概览:v- 前缀的命令

指令是什么

**指令(directive)**是以 v- 开头的特殊属性,告诉 Vue"对这个元素做什么"。它们像 HTML 自带属性,但 Vue 会解析并执行相应逻辑。

指令 作用 示例
v-bind 单向绑定属性 v-bind:href="url"
v-on 绑定事件 v-on:click="handler"
v-if / v-show 条件渲染 v-if="visible"
v-for 列表渲染 v-for="item in list"
v-model 表单双向绑定 v-model="username"
v-text / v-html 文本 / HTML 输出 v-html="content"

v-bind 与 v-on 的简写 ⭐

两个最高频的指令有简写,必须认识:

<a v-bind:href="url">链接</a>      <!-- 完整写法 -->
<a :href="url">链接</a>            <!-- 简写:冒号 -->

<button v-on:click="doSomething">按钮</button>  <!-- 完整写法 -->
<button @click="doSomething">按钮</button>      <!-- 简写:@ -->

:属性 绑定:把某个属性值与 data 数据关联,数据变属性跟着变。@事件 绑定:给元素挂事件处理。

<template>
  <img :src="coverUrl" :alt="coverAlt" />   <!-- 动态图片地址 -->
  <input :disabled="isDisabled" />          <!-- 动态禁用状态 -->
</template>

⚠️ 常见错误

  1. 忘记冒号/@ 简写规则href="url" 会把字符串 "url" 当字面量,而不是变量值;要用 :href="url"
  2. 把指令当普通属性v-if="false" 中 false 是 JS 布尔值,不是字符串,别加引号写成 v-if="'false'"(那永远为真)。
  3. 事件处理写括号时机@click="add(1)" 传参要括号,@click="add" 不传参不写括号;都写在 data 里的方法名。

1.3 条件渲染:v-if 与 v-show ⭐

v-if / v-else-if / v-else

按条件决定是否渲染某个元素,条件不成立时元素根本不在 DOM 里

<template>
  <p v-if="score >= 90">优秀</p>
  <p v-else-if="score >= 60">及格</p>
  <p v-else>加油</p>
</template>

三个指令必须紧挨着,中间不能插入其他元素,否则 v-else 匹配不到前面的 v-if。条件为假时,元素不渲染(DOM 里没有)。

v-show:只是藏起来

v-show 通过 CSS 的 display: none 隐藏元素——元素始终在 DOM 里,只是看不见

<p v-show="visible">我一直在 DOM 里,只是可能被藏起来</p>

怎么选 ⭐

  • 频繁切换(如弹窗、菜单展开收起):用 v-show,只切 display,开销小。
  • 很少切换、初始条件为假、或想省内存:用 v-if,完全不渲染更省。
  • 附带注意v-if 切换时会触发组件的创建/销毁(生命周期相关,见后续讲义),v-show 不会。

⚠️ 常见错误

  1. v-elsev-if 之间有其他元素:中间隔着 <br> 或注释,导致 v-else 失效。
  2. 高频切换用 v-if:每次都重建/销毁,开销大于 v-show 的 display 切换。
  3. v-show 用在很重的组件上:它一直存在,组件内部逻辑一直运行,要考虑代价。
  4. 混淆二者:面试常问"v-if 与 v-show 区别"——一个不渲染,一个 display:none

1.4 列表渲染:v-for ⭐

遍历数组与对象

v-for 是渲染列表的核心指令,把数组每一项都"生成"一份模板:

<template>
  <ul>
    <li v-for="item in todos" :key="item.id">
      {{ item.text }}
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      todos: [
        { id: 1, text: '买牛奶' },
        { id: 2, text: '写作业' },
        { id: 3, text: '锻炼' },
      ],
    };
  },
};
</script>

也可以拿到索引,并遍历对象:

<li v-for="(item, index) in todos" :key="item.id">
  {{ index }} - {{ item.text }}
</li>

<!-- 遍历对象:key 是属性名,value 是值 -->
<div v-for="(value, key) in user" :key="key">{{ key }}: {{ value }}</div>

key:列表的"身份证" ⭐

:key 给每个列表项一个稳定且唯一的标识,让 Vue 能精确追踪哪一项变了、哪一项被删了。它直接决定列表更新效率和正确性。

  • 数据里的唯一 iditem.id)最优。
  • 不要用索引 index 当 key:当列表发生插入/删除时,index 会"串位",导致复用错乱(比如输入框里的内容对错行)。

⚠️ 常见错误

  1. 忘记 :key:Vue 会警告,且更新时全量重建或复用错乱。
  2. index 当 key:列表顺序一变就出诡异 bug,尤其是包含表单的列表项。
  3. v-forv-if 同层使用v-ifv-for 同元素上优先级难记,容易冲突。正确姿势:用计算属性先过滤,再 v-for(见本讲 1.6)。
  4. 遍历对象时 value/key 顺序写反(key, value)(value, key) 千万别记错,数组才有 index。

1.5 模板里的表达式与过滤器思想

简洁的插值表达式

模板里可以写简单表达式做轻量处理:

<p>{{ price * quantity }} 元</p>
<p>{{ isLoggedIn ? '欢迎回来' : '请登录' }}</p>

复杂逻辑不该出现在模板里。模板应保持"易读",一旦表达式超过一两步,就应该抽出去,用计算属性(下一讲详解)处理。

v-cloak:防闪烁

初次加载时,模板可能短暂显示"原始双大括号"。用 v-cloak + CSS 隐藏未编译的模板:

[v-cloak] { display: none; }   /* 未编译完成的模板先藏起来 */
<p v-cloak>{{ name }}</p>

网络慢时用户不会看到一闪而过的 {{ name }},提升体验。

⚠️ 常见错误

  1. 模板里写大量逻辑:又长又难读,改需求时要反复"翻译",维护成本高。
  2. 三元表达式滥用:嵌套三层以上,没人读得懂,抽出去更好。
  3. 忘了 v-cloak 配 CSS:只写 v-cloak 属性不写 CSS,等于没加。

1.6 综合练习:带过滤的待办列表

用本讲知识做一个"只显示未完成项"的列表。这里先演示不推荐的写法(v-if 与 v-for 同层),再给出推荐写法:

<!-- ❌ 不推荐:v-if 与 v-for 同层 -->
<li v-for="item in todos" v-if="!item.done" :key="item.id">
  {{ item.text }}
</li>
<!-- ✅ 推荐:先过滤,再渲染 -->
<template>
  <ul>
    <li v-for="item in unfinished" :key="item.id">{{ item.text }}</li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      todos: [
        { id: 1, text: '买牛奶', done: false },
        { id: 2, text: '写作业', done: true },
        { id: 3, text: '锻炼', done: false },
      ],
    };
  },
  computed: {
    unfinished() {
      return this.todos.filter((item) => !item.done);
    },
  },
};
</script>

把"过滤逻辑"放到计算属性里,模板只剩干净的 v-for。这也是"模板只做展示、逻辑进 JS"的体现。

⚠️ 常见错误

  1. 过滤逻辑写进模板:模板变成"for 里套 if",可读性差,也容易在 v-if/v-for 优先级上踩坑。
  2. 直接修改 props 或 data 的副本.filter 返回新数组没问题,但直接用 .splice 原地改要格外小心。
  3. key 不稳:过滤后 index 会变化,务必用稳定 id 当 key。

1.7 样式绑定:让 class 和 style 也"活"起来

场景:状态决定样式

页面里经常要"根据数据给元素套不同的样式":选中高亮、按钮禁用、错误提示变红。除了用指令控制渲染,Vue 还提供了把 class / style 与数据绑定的能力。

:class 的三种写法

:class 可以绑定字符串、对象或数组,最常用的是对象语法——键是类名,值是布尔条件:

<template>
  <!-- 对象语法:isActive 为真时加上 active 类 -->
  <div :class="{ active: isActive }">内容</div>

  <!-- 同时绑定静态类与动态类,互不冲突 -->
  <div class="base" :class="{ active: isActive, error: hasError }">内容</div>

  <!-- 数组语法:可组合多个类 -->
  <div :class="['base', isActive ? 'active' : '']">内容</div>
</template>

对象语法最常用:"条件成立就加这个类",语义一目了然。

:style 绑定行内样式

:style 绑定一个对象,对象的属性名用驼峰写法(如 fontSize),适合少量、动态的样式:

<template>
  <div :style="{ fontSize: size + 'px', color: fontColor }">文字</div>
</template>

<script>
export default {
  data() {
    return { size: 16, fontColor: '#e8590c' };
  },
};
</script>

用计算属性整理样式逻辑

当样式条件复杂时,把判断抽进计算属性,模板保持干净:

<template>
  <div :class="itemClass">待办项</div>
</template>

<script>
export default {
  data() {
    return { done: false };
  },
  computed: {
    itemClass() {
      return { 'todo-item': true, 'todo-done': this.done };
    },
  },
};
</script>

为什么不直接写死 class

直接写死静态类,页面永远一个样;用数据驱动 class,样式成为"状态的投影",才能实现"数据一变,外观自动跟着变"——这正是 MVVM 思想在样式上的延伸。

⚠️ 常见错误

  1. :class 里忘写 ::写成 class="{ active: isActive }",会把整串字符串当静态类名,完全不生效。
  2. 对象语法的值写成字符串:class="{ active: 'isActive' }"'isActive' 是字符串永远为真,应写布尔表达式 isActive
  3. :style 属性名用短横线{ 'font-size': ... } 也能用但需加引号,驼峰 fontSize 更自然。
  4. 样式逻辑全堆模板:条件多时模板又长又难读,抽到计算属性。

1.8 模板背后的渲染机制

模板不是"直接塞进页面"

你可能以为模板就是"字符串拼起来塞进 HTML"。实际上 Vue 会先把模板编译(compile)成一个渲染函数(render function),渲染函数再根据数据生成"虚拟 DOM",最后才更新真实页面。

模板 → 编译 → 渲染函数 → 虚拟 DOM → 真实 DOM

这个过程对使用者是透明的,但理解它能解释很多现象。

虚拟 DOM:为什么更新那么快

直接操作真实 DOM 很慢(浏览器要重排、重绘)。Vue 维护一棵虚拟 DOM(virtual DOM)——用普通 JS 对象描述页面结构,比真实 DOM 轻得多。数据变化时:

  1. 重新执行渲染函数,生成新的虚拟 DOM
  2. 与上一次的虚拟 DOM 做对比(diff),找出差异。
  3. 只把有差异的部分更新到真实 DOM。

这就是"精准更新"的来源——不是整页重绘,而是只动该动的地方。这也是前面强调 :key 的原因:diff 对比时靠 key 判断"哪些是同一项",key 合理,对比才高效准确。

为什么模板里不能写复杂逻辑

渲染函数每次数据变化都会执行,模板里的复杂表达式会重复求值,影响性能;同时模板本应"一眼看懂",塞满逻辑没人维护。把逻辑放进计算属性,利用它的缓存,既快又清晰——性能与可读性同时保证。

一个判断技巧

看到"页面更新慢"或"卡顿",先想三件事:是不是模板里有重计算?列表 key 是否合理?是否做了不必要的全量更新? 多数性能问题都出在这三处。

⚠️ 常见错误

  1. 在模板里调用耗时的函数:每次渲染都执行,拖慢更新;应放计算属性或抽离。
  2. 不理解 diff 而轻视 key:key 用错会导致虚拟 DOM 对比错乱,产生难以复现的显示 bug。
  3. 把虚拟 DOM 想成"HTML 字符串":它是 JS 对象结构,理解这一点才能读懂渲染原理。

📌 记忆口诀

  • 插值{{ 表达式 }} 只填值,语句别进来。
  • 指令v-bind 改属性、v-on 绑事件,简写 :@
  • 条件v-if 不渲染,v-show 只是藏;频繁切换用 v-show
  • 列表v-for:key,key 用 id 不用 index。
  • 模板职责:只做展示,逻辑进 JS。

📌 双语术语表(本讲)

中文 English 记忆点
模板 template 带数据的 HTML 骨架
插值 interpolation {{ }} 填数据
指令 directive v- 开头的特殊属性
条件渲染 conditional rendering v-if / v-show
列表渲染 list rendering v-for
双向绑定 two-way binding v-model
单向绑定 one-way binding v-bind
事件绑定 event binding v-on / @
key 列表项的唯一标识

⭐ 本讲考点清单

  1. 插值 {{ }}:写表达式、不写语句、自动更新
  2. v-textv-html 的区别及 v-html 的安全风险
  3. v-bind:)与 v-on@)的完整写法与简写
  4. v-if / v-else-if / v-else 的连用规则(必须紧挨)
  5. v-ifv-show 的区别及选择原则
  6. v-for 遍历数组(值/索引)与对象(值/键)的写法
  7. :key 的作用、为何用 id 不用 index
  8. 模板表达式保持简洁、逻辑交给计算属性
  9. v-ifv-for 不同层的正确姿势