【Vue3】基础渲染与模板语法
一、SFC 与模板语法
1.1 Vue 单文件组件(SFC)结构
语法:
<template>
<!-- 放 HTML 模板 —— 写页面结构和样式绑定 -->
</template>
<script setup>
// 放 JavaScript 逻辑 —— 写数据、函数、生命周期
</script>
<style scoped>
/* 放 CSS 样式 —— 只对当前组件生效 */
</style>
一个 .vue 文件就是一个单文件组件(Single File Component,简称 SFC)。它把三个东西写在一个文件里:
<template>:长得像 HTML,但可以嵌入 Vue 的特殊语法<script setup>:JavaScript/TypeScript 逻辑<style scoped>:CSS 样式,scoped让样式只作用于当前组件
详细讲解:
传统 Web 开发中,HTML、JS、CSS 分三个文件:
index.html style.css script.js
(结构) (样式) (逻辑)
Vue 的 SFC 把三者合进一个文件,因为一个组件就是一个独立单元——它的模板、逻辑、样式天然绑定在一起。这叫单文件组件,是 Vue 的核心组织方式。
<template>
<div class="card">
<h1>{{ title }}</h1>
<p>{{ content }}</p>
</div>
</template>
<script setup>
const title = 'Hello Vue3';
const content = '这是我的第一个组件';
</script>
<style scoped>
.card {
border: 1px solid #ddd;
padding: 16px;
}
.card h1 {
color: #42b883;
}
</style>
1.2 <script setup> 语法糖
语法:
<script setup>
// 所有顶层变量、函数都自动暴露给模板使用
const count = ref(0);
function increment() { count.value++; }
</script>
<template>
<button @click="increment">{{ count }}</button>
</template>
<script setup> 是 Vue3 推荐写法,在 <script> 标签上加 setup 关键字,里面声明的所有变量和函数都自动能在模板中使用,不需要手动 return。
详细讲解:
Vue3 有两种写法:
<!-- 写法一:选项式 API(Vue2 风格,Vue3 仍支持但不推荐) -->
<script>
export default {
data() {
return { count: 0 };
},
methods: {
increment() { this.count++; }
}
}
</script>
<!-- 写法二:组合式 API + <script setup>(Vue3 推荐写法) -->
<script setup>
const count = ref(0);
function increment() { count.value++; }
</script>
<script setup> 对比选项式 API 的好处:
| 选项式 API | <script setup> |
|
|---|---|---|
| 写法 | 必须 export default 对象 | 直接写变量和函数 |
| 暴露给模板 | 必须写在 data/methods 里 | 顶层声明自动暴露 |
| this | 大量使用 this | 不需要 this |
| 逻辑复用 | mixins(容易冲突) | composables(灵活) |
| TypeScript 支持 | 一般 | 更好 |
为什么不推荐选项式 API 了?因为当组件变大时,data、methods、computed 被分散在不同的选项中,同一个功能的代码被拆散。<script setup> 让相关代码聚在一起,按功能组织而不是按类型组织。
1.3 <template> 多根节点
语法:
<!-- Vue2:必须用一个根元素包裹 -->
<template>
<div>
<h1>标题</h1>
<p>内容</p>
</div>
</template>
<!-- Vue3:可以直接写多个兄弟节点,不需要外层包裹 -->
<template>
<h1>标题</h1>
<p>内容</p>
</template>
Vue2 中 <template> 下必须有一个根元素(比如一个 <div> 包住所有内容)。Vue3 取消了这一限制,可以写多个平级节点。
详细讲解:
<template>
<header>网站头部</header>
<main>主要内容区域</main>
<footer>网站底部</footer>
</template>
好处是少一层无意义的 DOM 嵌套,生成的 HTML 更干净。Vue 内部用 Fragment(片段)来渲染多个根节点。
1.4 <style scoped>
语法:
<style scoped>
/* 这里的 CSS 只对当前组件生效,不会影响其他组件 */
.button { background: blue; }
</style>
<style>
/* 不加 scoped:全局 CSS,会影响所有组件 */
.button { background: red; }
</style>
scoped 是 Vue 提供的样式隔离机制。加了 scoped 后,Vue 会自动给当前组件的每个 DOM 元素加上一个独特的数据属性(如 data-v-abc123),并修改你的 CSS 选择器来匹配这个属性。
详细讲解:
不用的后果(全局样式污染):
/* 不加 scoped:你在组件 A 中写的 .title 样式,
会影响组件 B 中同样叫 .title 的元素 */
.title { color: red; }
加了 scoped 后:
<template>
<h1 class="title">标题</h1>
</template>
<style scoped>
/* 编译后实际变成:.title[data-v-7ba5bd90] { color: red; } */
/* 自动加了属性选择器,只匹配当前组件的元素 */
.title { color: red; }
</style>
父子组件样式穿透(进阶,了解即可):有时候你需要在父组件中修改子组件的内部样式,可以用 :deep():
<style scoped>
/* 穿透 scoped 限制,影响子组件内部元素 */
:deep(.child-class) { color: red; }
</style>
1.5 v-bind 与 v-on
v-bind —— 动态绑定 HTML 属性
语法:
v-bind:属性名="JavaScript表达式"
// 简写(推荐)
:属性名="JavaScript表达式"
v-bind 把 HTML 属性的值和一个 JavaScript 表达式绑定起来。表达式的结果会作为属性的值。
详细讲解:
<template>
<!-- 静态属性:属性值写死 -->
<img src="/logo.png" alt="Logo">
<!-- 动态属性:属性值从 data 中来 -->
<img :src="imageUrl" :alt="imageAlt">
<!-- 布尔属性:表达式为真时才添加该属性 -->
<button :disabled="isSubmitting">提交</button>
<!-- 绑定多个属性(一个对象) -->
<div v-bind="attrs"></div>
<!-- 相当于 :id="attrs.id" :class="attrs.class" :style="attrs.style" -->
</template>
<script setup>
const imageUrl = ref('https://example.com/photo.jpg');
const imageAlt = ref('风景照片');
const isSubmitting = ref(false);
const attrs = { id: 'main', class: 'container', style: 'color: red' };
</script>
为什么要用 v-bind?没有它,<img src="imageUrl"> 中的 imageUrl 会被当作字符串字面量处理,而不是变量。:src="imageUrl" 才表示"取 JavaScript 变量 imageUrl 的值"。
v-on —— 监听 DOM 事件
语法:
v-on:事件名="事件处理函数"
// 简写(推荐)
@事件名="事件处理函数"
// 带参数
@事件名="处理函数(参数)"
// 内联语句
@事件名="count++"
v-on 监听 DOM 事件(点击、输入、鼠标移动等),触发时执行对应的 JavaScript。
详细讲解:
<template>
<!-- 基本用法:点击执行函数 -->
<button @click="handleClick">点击</button>
<!-- 带参数 -->
<button @click="deleteItem(item.id)">删除</button>
<!-- 传事件对象 $event -->
<input @input="handleInput($event)">
<!-- 内联语句(简单逻辑直接写,不需要定义函数) -->
<button @click="count++">+1</button>
<!-- 事件修饰符 -->
<form @submit.prevent="onSubmit"> <!-- .prevent 阻止默认行为 -->
<a @click.stop="goHome">返回</a> <!-- .stop 阻止事件冒泡 -->
<input @keyup.enter="submit"> <!-- .enter 只在按回车时触发 -->
</template>
事件修饰符一览:
| 修饰符 | 作用 | 等价于 |
|---|---|---|
.stop |
阻止事件冒泡 | event.stopPropagation() |
.prevent |
阻止默认行为 | event.preventDefault() |
.once |
事件只触发一次 | |
.enter |
只在按回车键时触发 | |
.esc |
只在按 Esc 时触发 |
1.6 动态 class / style 绑定
语法:
// class 绑定
:class="{ 类名: 布尔条件 }" ← 对象语法
:class="[ 类名1, 类名2 ]" ← 数组语法
:class="计算属性或变量" ← 直接绑定
// style 绑定
:style="{ CSS属性: 值 }" ← 对象语法
:style="[ 样式对象1, 样式对象2 ]" ← 数组语法
这是 Vue 模板中最常用的功能之一——根据数据状态动态切换样式。
详细讲解:
<template>
<!-- ===== class 对象语法 ===== -->
<!-- active 为 true 时添加 active 类,否则去掉 -->
<div :class="{ active: isActive, disabled: isDisabled }">
内容
</div>
<!-- ===== class 数组语法 ===== -->
<div :class="['base-class', isActive ? 'active' : '', themeClass]">
内容
</div>
<!-- ===== style 对象语法 ===== -->
<!-- CSS 属性用驼峰命名(backgroundColor 而非 background-color) -->
<div :style="{ color: textColor, fontSize: fontSize + 'px', background: bgColor }">
内容
</div>
<!-- ===== style 数组语法(多个样式对象合并) ===== -->
<div :style="[baseStyles, activeStyles]">内容</div>
</template>
<script setup>
const isActive = ref(true);
const isDisabled = ref(false);
const themeClass = ref('dark-theme');
const textColor = ref('#333');
const fontSize = ref(16);
const bgColor = ref('#fff');
const baseStyles = { padding: '10px', margin: '5px' };
const activeStyles = { border: '1px solid blue' };
</script>
为什么需要动态 class 绑定?没有它,你需要写:
// ❌ 手动拼接 class 字符串
`base-class ${isActive ? 'active' : ''} ${themeClass}`
// ✅ Vue 的 :class 对象和数组语法更清晰可读
:class="['base-class', { active: isActive }, themeClass]"
二、响应式基础与列表渲染
2.1 ref —— 基本响应式数据
语法:
import { ref } from 'vue';
const 变量名 = ref(初始值);
// 读取值
变量名.value
// 修改值
变量名.value = 新值
ref() 把一个普通值变成"响应式"的——当 .value 改变时,任何用到这个值的地方(模板、计算属性、侦听器)都会自动更新。
详细讲解:
为什么需要 ref?普通的 JavaScript 变量不是响应式的:
// ❌ 普通变量:改了,但页面不会更新
let count = 0;
count = 1; // 页面还是显示 0
// ✅ ref:修改 .value,页面自动更新
const count = ref(0);
count.value = 1; // 页面自动变成 1
ref 支持任意类型:
// 基本类型
const count = ref(0);
const name = ref('Alice');
const isDone = ref(false);
// 对象类型(ref 内部会自动把对象交给 reactive 处理)
const user = ref({ name: 'Alice', age: 25 });
// 读取和修改
console.log(count.value); // 0
count.value = 10;
console.log(user.value.name); // 'Alice'
user.value.age = 26;
// 数组
const todos = ref([{ id: 1, text: '学习 Vue' }]);
todos.value.push({ id: 2, text: '做项目' });
.value 的规则:
| 场景 | 写法 |
|---|---|
在 <script setup> 中读取 |
变量名.value |
在 <script setup> 中修改 |
变量名.value = 新值 |
在 <template> 中 |
不需要 .value,Vue 自动解包 |
<script setup>
const count = ref(0);
function increment() {
count.value++; // script 中要写 .value
}
</script>
<template>
<!-- template 中不需要 .value,直接写变量名 -->
<p>{{ count }}</p>
<button @click="increment">+1</button>
</template>
为什么模板中不用 .value?Vue 在模板编译时自动对 ref 进行解包——把 count 展开为 count.value。
2.2 reactive —— 深层响应式对象
语法:
import { reactive } from 'vue';
const 变量名 = reactive({ 键1: 值1, 键2: 值2 });
// 直接访问属性,不需要 .value
变量名.键1
变量名.键1 = 新值
reactive() 把一个对象变成深层响应式的——对象中任何层级的属性发生变化,视图都会更新。与 ref 不同,reactive 不需要 .value。
详细讲解:
const user = reactive({
name: 'Alice',
age: 25,
address: {
city: 'Beijing',
zip: '100000'
}
});
// 直接访问属性,不需要 .value
console.log(user.name); // 'Alice'
console.log(user.address.city); // 'Beijing'
// 修改任何层级的属性,都会触发更新
user.age = 26;
user.address.city = 'Shanghai';
ref vs reactive 怎么选:
| ref | reactive | |
|---|---|---|
| 支持类型 | 任意类型(基本类型 + 对象) | 只能是对象 |
| 读写方式 | .value |
直接读写属性 |
| 模板中 | 自动解包,不用 .value |
直接读写属性 |
| 重新赋值 | ✅ 可以 变量.value = 全新的值 |
❌ 不能直接替换整个对象 |
| 解构 | 解构后仍保持响应式 | 解构后失去响应式(需要用 toRefs) |
实际建议: Vue 官方现在推荐优先用 ref,因为:
- 写法统一(全都
.value) - 支持基本类型
- 可以整体替换
- 解构更方便
reactive 适合你明确知道不会替换整个对象且不会解构的场景。
// 大多数场景用 ref 就够了
const count = ref(0);
const user = ref({ name: 'Alice', age: 25 });
const list = ref([1, 2, 3]);
// 只有特定场景用 reactive
const state = reactive({
items: [],
loading: false,
error: null
});
2.3 computed —— 计算属性
语法:
import { computed } from 'vue';
const 变量名 = computed(() => {
// 依赖其他响应式数据,返回计算结果
return 计算结果;
});
// 只读,不能直接赋值
// 变量名.value = xxx ❌ 会报错
computed() 定义了一个基于其他响应式数据自动计算的值。它只在其依赖的数据发生变化时重新计算,否则返回缓存的结果。
详细讲解:
直接在模板中写表达式:
<p>{{ message.split('').reverse().join('') }}</p>
这样写的问题:如果多处用到同样的计算结果,要重复写同样的表达式,可读性差且每次渲染都要重新计算。
用 computed:
<script setup>
const firstName = ref('张');
const lastName = ref('三');
const age = ref(25);
// computed 自动追踪依赖的响应式数据
const fullName = computed(() => `${firstName.value}${lastName.value}`);
const description = computed(() => `${fullName.value},${age.value}岁`);
// 列表过滤
const todos = ref([
{ id: 1, text: '学习 Vue', done: true },
{ id: 2, text: '做项目', done: false },
{ id: 3, text: '写文档', done: false }
]);
const activeTodos = computed(() => todos.value.filter(t => !t.done));
</script>
<template>
<p>{{ fullName }}</p> <!-- 张三 -->
<p>{{ description }}</p> <!-- 张三,25岁 -->
<ul>
<li v-for="todo in activeTodos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</template>
computed vs 普通函数:
| computed | 普通函数 | |
|---|---|---|
| 缓存 | ✅ 依赖不变时返回缓存结果 | ❌ 每次调用都重新计算 |
| 模板调用 | 直接写变量名 {{ fullName }} |
加括号 {{ getFullName() }} |
| 自动追踪 | ✅ 自动追踪依赖 | ❌ 不追踪 |
<template>
<!-- computed —— 只有在 todos 或 filter 变化时才重新计算 -->
<p>{{ filteredTodos.length }}</p>
<!-- 普通函数 —— 每次渲染都执行一遍 -->
<p>{{ getFilteredTodos().length }}</p>
</template>
所以当计算有性能开销(遍历、过滤、排序)时,应该用 computed。
2.4 条件渲染 v-if / v-show
语法:
<!-- v-if:条件为真才渲染元素(真正的创建/销毁) -->
<元素 v-if="条件">内容</元素>
<元素 v-else-if="条件2">内容</元素>
<元素 v-else>内容</元素>
<!-- v-show:始终渲染元素,只是切换 display 的显示/隐藏 -->
<元素 v-show="条件">内容</元素>
两者都根据条件决定元素是否可见,但实现机制不同。
详细讲解:
<template>
<!-- ===== v-if 系列 ===== -->
<!-- 条件为 true 时 DOM 中存在,false 时 DOM 中完全不存在 -->
<p v-if="score >= 90">优秀</p>
<p v-else-if="score >= 60">及格</p>
<p v-else>不及格</p>
<!-- ===== v-show ===== -->
<!-- DOM 中始终存在,只是 display: none -->
<p v-show="isVisible">这条内容始终在 DOM 中,只是隐藏了</p>
<!-- ===== <template> 上的条件渲染 ===== -->
<!-- v-if 可以写在 <template> 上(不会额外产生 DOM 节点) -->
<template v-if="hasHeader">
<h1>标题</h1>
<p>副标题</p>
</template>
</template>
v-if vs v-show 对比:
| v-if | v-show | |
|---|---|---|
| 渲染机制 | 条件为真时才渲染到 DOM | 始终渲染,切换 display |
| 切换成本 | 高(销毁/重建 DOM) | 低(只改 CSS) |
| 初始渲染成本 | 低(条件为假时不渲染) | 高(始终渲染) |
| 适用场景 | 运行时很少切换 | 频繁切换显示/隐藏 |
配合 v-else |
✅ 支持 | ❌ 不支持 |
怎么选:
<!-- 用户登录状态切换 —— 不频繁,用 v-if -->
<AdminPanel v-if="user.role === 'admin'" />
<!-- 下拉菜单展开收起 —— 频繁切换,用 v-show -->
<div v-show="menuOpen">
<!-- 菜单内容 -->
</div>
2.5 v-for —— 列表渲染
语法:
<!-- 遍历数组 -->
<元素 v-for="(当前项, 索引) in 数组" :key="唯一标识">
{{ 当前项 }}
</元素>
<!-- 遍历对象 -->
<元素 v-for="(值, 键, 索引) in 对象" :key="键">
{{ 键 }}: {{ 值 }}
</元素>
<!-- 遍历数字范围(1 到 N) -->
<元素 v-for="n in 10" :key="n">
{{ n }}
</元素>
v-for 根据数据列表生成重复的 DOM 元素。:key 是必须的,帮助 Vue 高效追踪每个列表项的身份。
详细讲解:
<template>
<!-- ===== 基本列表 ===== -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
<!-- ===== 带索引 ===== -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.text }}
</li>
</ul>
<!-- ===== 遍历对象 ===== -->
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
const items = ref([
{ id: 1, text: '学习 Vue' },
{ id: 2, text: '做项目' },
{ id: 3, text: '写文档' }
]);
const user = ref({
name: 'Alice',
age: 25,
role: 'admin'
});
</script>
为什么必须用 :key?
想象一个场景:列表有 3 项 [A, B, C],你在最前面插入一项 X,变成 [X, A, B, C]。
没有 :key,Vue 不会知道哪些是新增、哪些是移动,会逐个更新已有元素的内容(A→X, B→A, C→B, 再新增 C),效率低且会引发 UI 闪烁。
有 :key(用唯一 ID),Vue 知道 X 是新的、A/B/C 只是向后移动了,它直接移动已有的 DOM 节点到新位置,只新增 X。
:key 的选择:
<!-- ✅ 用数据自带的唯一 ID(最好) -->
<li v-for="item in list" :key="item.id">{{ item.text }}</li>
<!-- ✅ 用索引(仅当列表静态不变化时) -->
<li v-for="(item, index) in list" :key="index">{{ item.text }}</li>
<!-- ❌ 不要用随机数或 Math.random() -->
<!-- 每次渲染 key 都变,等于完全重新创建所有元素 -->
<li v-for="item in list" :key="Math.random()">{{ item.text }}</li>
v-for 和 v-if 不要同时用在同一个元素上:
<!-- ❌ 错误:v-for 优先级高于 v-if,每次都会遍历全部再判断 -->
<li v-for="item in items" v-if="item.isActive" :key="item.id">
{{ item.text }}
</li>
<!-- ✅ 正确:用 computed 先过滤再遍历 -->
<li v-for="item in activeItems" :key="item.id">
{{ item.text }}
</li>
<script setup>
const activeItems = computed(() => items.value.filter(i => i.isActive));
</script>
2.6 综合示例 —— Todo List 核心逻辑
把本阶段学到的知识点串联起来:
<script setup>
import { ref, computed } from 'vue';
// ref 声明响应式数据
const newTodo = ref('');
const todos = ref([
{ id: 1, text: '学习 ref 和 reactive', done: true },
{ id: 2, text: '掌握 v-for 和 :key', done: false },
{ id: 3, text: '理解 computed', done: false }
]);
// computed 派生状态
const stats = computed(() => {
const total = todos.value.length;
const done = todos.value.filter(t => t.done).length;
return { total, done, percent: total ? Math.round(done / total * 100) : 0 };
});
// 函数
function addTodo() {
if (!newTodo.value.trim()) return;
todos.value.push({ id: Date.now(), text: newTodo.value, done: false });
newTodo.value = '';
}
function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id);
if (todo) todo.done = !todo.done;
}
function removeTodo(id) {
todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<template>
<div class="todo-app">
<!-- v-model 双向绑定输入框(下阶段详解) -->
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办">
<button @click="addTodo">添加</button>
<!-- v-for 渲染列表,:key 绑定唯一 id -->
<!-- :class 动态绑定完成样式 -->
<ul>
<li v-for="todo in todos" :key="todo.id"
:class="{ completed: todo.done }">
<input type="checkbox" :checked="todo.done" @change="toggleTodo(todo.id)">
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<!-- v-if/v-else 条件渲染 -->
<p v-if="todos.length === 0">暂无待办</p>
<p v-else>完成 {{ stats.done }}/{{ stats.total }}({{ stats.percent }}%)</p>
</div>
</template>
<style scoped>
.completed span { text-decoration: line-through; color: #999; }
.todo-app { max-width: 400px; margin: 20px auto; }
ul { list-style: none; padding: 0; }
li { padding: 8px 0; border-bottom: 1px solid #eee; }
</style>
这个示例包含了本阶段的核心知识点:
ref声明响应式数据computed派生计算结果v-for列表渲染 +:keyv-if/v-else条件渲染:class动态样式绑定@click/@keyup.enter事件监听<style scoped>组件样式隔离
更多推荐

所有评论(0)