Vue3 指令

什么是指令

Vue 指令(Directives)是 Vue.js 的一项核心功能,它们可以在 HTML 模板中以 v- 开头的特殊属性形式使用,用于将响应式数据绑定到 DOM 元素上或在 DOM 元素上进行一些操作。

Vue 指令是带有前缀 v- 的特殊 HTML 属性,它赋予 HTML 标签额外的功能。

指令本质上是对 DOM 操作的封装,让你可以通过声明式的方式控制 DOM。

指令的基本语法

1
<element v-directive:argument.modifier="expression"></element>
  • 指令名称v-directive(如 v-ifv-for
  • 参数argument(可选,如 v-bind:href 中的 href)参数在指令后以冒号指明。
  • 修饰符modifier(可选,如 v-on:click.stop 中的 stop)修饰符是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。
  • 表达式expression(指令的值,可以是变量、表达式或方法)

指令存在简写,例如,如 v-bind 简写为 :v-on简写为@`

Vue3的内置指令集

内容渲染指令

  • v-text:设置元素的文本内容,它会覆盖标签内的原有内容,将内容作为纯文本处理

    1
    2
    3
    <p v-text="msg"></p>
    <!-- 等同于 -->
    <p>{{msg}}</p> <!-- 但 v-text 会覆盖标签内原有内容,而插值表达式不会 -->
  • v-html

    渲染 HTML 内容,,准去来说是更新元素的 innerHTML,将内容作为 HTML 解析,可以渲染标签,内容会按普通 HTML 插入——不会作为 Vue 模板进行编译。

    在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击永远不要将用户提交的内容(如评论区)赋值给 v-html。

    1
    <p v-html="htmlContent"></p>
    1
    2
    3
    data: {
    htmlContent: '<strong>加粗的文字</strong>'
    }

条件渲染指令

  • v-if:根据表达式的值条件性地渲染元素
  • v-elsev-if 的否定条件
  • v-else-ifv-if 的链式条件

上面三个都是根据表达式的真假值,有条件地渲染元素。

  • 这是真正的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件被适当地销毁和重建。

  • 它是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会渲染条件块。

  • 如果在运行时条件很少改变,或者条件判断逻辑复杂,使用 v-if。因为 v-if 有更高的切换开销,这涉及到DOM 的增删,而 v-show 有更高的初始渲染开销,因为所有元素都要渲染,只是藏起来

    1
    2
    3
    <div v-if="type === 'A'">A</div>
    <div v-else-if="type === 'B'">B</div>
    <div v-else>Not A/B</div>
  • v-show:根据表达式的真假值,切换元素的 display CSS 属性。

    • 元素始终会被渲染并保留在 DOM 中,只是通过 CSS display: none 进行隐藏。
    • 场景:适合非常频繁地切换显示/隐藏的场景。
    1
    <h1 v-show="ok">Hello!</h1>

列表渲染指令

  • v-for:基于一个数组或对象来渲染一个列表。

    语法item in items(item, index) in items

    • 使用 key:总是推荐给 v-for 提供一个唯一的 :key 属性,以便 Vue 的 Diff 算法能追踪每个节点的身份,从而重用和重新排序现有元素,提升性能。
    • 优先级:在 Vue 3 中,v-if 的优先级高于 v-for(这与 Vue 2 相反)。这意味着你不能在同一个元素上同时使用它们来过滤列表,应该先在计算属性(computed)中过滤好数组,再遍历。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 遍历数组 -->
    <li v-for="(item, index) in items" :key="item.id">
    {{ index }} - {{ item.message }}
    </li>

    <!-- 遍历对象 -->
    <li v-for="(value, key, index) in myObject" :key="key">
    {{ index }}. {{ key }}: {{ value }}
    </li>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    data: {
    items: [
    { message: 'Foo' },
    { message: 'Bar' }
    ],
    object: {
    title: 'How to do lists in Vue',
    author: 'Jane Doe',
    publishedAt: '2016-04-10'
    }
    }

事件和属性绑定指令

v-bind

  • 有缩写,缩写为:

  • 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。

  • 在绑定 class 或 style 时,支持数组或对象语法。

  • Vue 3.2+ 新特性:如果没有传值,默认值为 true(同名简写)。<div :id> 等同于 <div :id="id">

    1
    2
    3
    4
    5
    <!-- 绑定 src -->
    <img :src="imageSrc" />

    <!-- 动态 class (对象语法) -->
    <div :class="{ active: isActive, 'text-danger': hasError }"></div>

v-on

  • 缩写:@

  • 作用:绑定事件监听器。

  • 修饰符:Vue 提供了丰富的修饰符,例如:

    • .stop - 调用 event.stopPropagation()
    • .prevent - 调用 event.preventDefault()
    • .once - 只触发一次回调。
    • .enter - 监听键盘 Enter 键。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <!-- 基本用法 -->
    <button v-on:click="counter += 1">增加 1</button>
    <!-- 简写形式 -->
    <button @click="counter += 1">增加 1</button>

    <!-- 方法调用 -->
    <button @click="greet">Greet</button>

    <!-- 事件修饰符 -->
    <form @submit.prevent="onSubmit">...</form>
    <a @click.stop="doThis"></a>

v-model

  • 作用:在表单输入元素或组件上创建双向数据绑定。本质上是一个语法糖,结合了 v-bind 和 v-on 的功能

  • 修饰符

    • .lazy - 取代 input 监听 change 事件(失去焦点时才更新)。
    • .number - 自动将用户的输入值转为数值类型。
    • .trim - 自动过滤用户输入的首尾空白字符。
  • Vue 3 变化:在组件上使用时,默认绑定的 prop 名由 value 变为了 modelValue,事件变为了 update:modelValue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <input v-model="message" placeholder="edit me">
    <p>Message is: {{ message }}</p>

    <!-- 复选框 -->
    <input type="checkbox" id="checkbox" v-model="checked">
    <label for="checkbox">{{ checked }}</label>

    <!-- 单选按钮 -->
    <input type="radio" id="one" value="One" v-model="picked">
    <label for="one">One</label>
    <input type="radio" id="two" value="Two" v-model="picked">
    <label for="two">Two</label>
    <p>Picked: {{ picked }}</p>

    <!-- 选择框 -->
    <select v-model="selected">
    <option disabled value="">请选择</option>
    <option>A</option>
    <option>B</option>
    </select>
    <span>Selected: {{ selected }}</span>

插槽类

v-slot

  • 缩写:#

  • 作用:用于声明具名插槽或是作用域插槽的出口。

  • 注意:只能用于<template> 元素上(只有一种例外情况:独占默认插槽)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <BaseLayout>
    <!-- 具名插槽 header -->
    <template #header>
    <h1>Page Title</h1>
    </template>

    <!-- 默认插槽 -->
    <p>Main content...</p>
    </BaseLayout>

性能优化与编译控制类

v-pre

  • 跳过这个元素和它的子元素的编译过程。

  • 用到它的时候基本就是用来显示原始 Mustache 标签({{ }}),或者用来跳过大量没有指令的静态节点以加快编译速度。

    1
    <span v-pre>{{ this will not be compiled }}</span>

v-once

  • 只渲染元素和组件一次

  • 随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

    1
    <span v-once>This will never change: {{ msg }}</span>

v-cloak

  • 用于隐藏未编译的 Mustache 标签直到 Vue 实例准备完毕。

  • 这个指令保持在元素上直到关联的组件实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,可以防止在页面加载时出现闪烁的 。

    1
    2
    3
    [v-cloak] {
    display: none;
    }
    1
    2
    3
    <div v-cloak>
    {{ message }}
    </div>

v-memo

  • Vue 3.2+ 新增

  • 缓存一个模板的子树。v-memo="[valueA, valueB]"

  • 如果数组中的每个值都和上次渲染时相同,则整个子树的更新会被跳过。这可以看作是手动版的 React useMemo 组件优化。

  • 主要用于性能极度敏感的场景,比如渲染包含成千上万个元素的长列表(搭配 v-for 使用)。

    1
    2
    3
    4
    <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
    <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
    <p>...more complex content...</p>
    </div>

    例如对这个例子,只有当 item.id === selected 的结果发生变化时,这个 div 才会重新渲染,否则直接复用缓存。

如何自定义指令

除了内置指令,Vue 还允许你创建自定义指令来封装可复用的 DOM 操作逻辑。

自定义指令允许我们自己定义以 v- 开头的指令,用于对普通 DOM 元素进行底层的操作。

什么时候使用?

  • 当且仅当需要直接操作 DOM 节点(例如聚焦输入框、改变样式、添加特定事件监听),且 Vue 的声明式模板无法轻松实现时。
  • 自定义指令用于处理底层 DOM 操作,不要滥用,能用数据驱动解决的优先用组件逻辑解决。

自定义指令和组件一样,分为全局注册局部注册

注册全局指令

在 main.js 中通过 app.directive 进行注册,注册后可以在项目任何组件中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.js
const app = createApp(App)

// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})

app.mount('#app')

注册局部指令

在组件的 directives 选项中注册。

1
2
3
4
5
6
7
8
9
10
// 在组件中
export default {
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
}

其实还有一致<script setup>的局部注册的方式

这是 Vue 3 最常用的方式。在<script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令

1
2
3
4
5
6
7
8
9
10
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>

<template>
<input v-focus />
</template>

关于 <script setup> 的相关内容我会在下面说

指令的生命周期钩子

自定义指令定义对象可以提供几个钩子函数(都是可选的),这些钩子函数和组件的生命周期非常相似:

  • created(el, binding, vnode): 在绑定元素的属性或事件监听器被应用之前调用。
  • **beforeMount(...)**: 在元素被插入到 DOM 前调用。
  • mounted(...): 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用。(最常用)
  • beforeUpdate(...): 绑定元素的父组件更新前调用。
  • updated(...): 在绑定元素的父组件及他自己的所有子节点都更新后调用。
  • beforeUnmount(...): 绑定元素的父组件卸载前调用。
  • unmounted(...): 绑定元素的父组件卸载后调用。(用于清理定时器或事件监听)

钩子函数的参数

每个钩子函数都会传入以下参数,其中 el 和 binding 最常用:

  1. el: 指令所绑定的元素,可以用来直接操作 DOM。
  2. binding: 一个对象,包含以下属性:
    • value: 传递给指令的值。例如 v-my-directive=“1 + 1” 中,值是 2。
    • oldValue: 之前的值,仅在 beforeUpdate 和 updated 中可用。
    • arg: 传递给指令的参数 (如果有的话)。例如 v-my-directive:foo 中,参数是 “foo”。
    • modifiers: 一个包含修饰符的对象 (如果有的话)。例如 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    • dir: 指令的定义对象。
1
<div v-demo:foo.bar="hello"></div>

在上面的例子中,binding 对象会是:

1
2
3
4
5
6
{
arg: 'foo',
modifiers: { bar: true },
value: 'hello',
dir: { ... }
}

但是在很多时候,我们可能只想在 mounted 和 updated 时触发相同的行为,而不关心其他的钩子。此时可以传递一个函数而不是对象:

1
2
3
4
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时调用
el.style.color = binding.value
})

使用方式

1
<div v-color="'red'">这段文字是红色的</div>

实际使用

光说不练假把式,我们来看几个 Vue3 项目中非常实用的自定义指令案例。

在后台管理系统中,我们经常需要根据用户的角色(Role)来控制按钮的显示与隐藏。

那么我们可以编写一个 v-permission 权限控制的指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// permission.js
import { useUserStore } from '@/stores/user' // 假设你用了 Pinia

export const vPermission = {
mounted(el, binding) {
const { value } = binding // 获取指令传过来的权限值,例如 ['admin', 'editor']
const userStore = useUserStore()
const userRoles = userStore.roles // 获取当前用户角色

// 判断用户角色是否在所需权限中
if (value && value instanceof Array && value.length > 0) {
const hasPermission = userRoles.some(role => {
return value.includes(role)
})

// 如果没有权限,直接从 DOM 中移除该元素
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
} else {
throw new Error(`需要指定权限,例如 v-permission="['admin','editor']"`)
}
}
}

使用它

1
<button v-permission="['admin']">只有管理员能看到这个按钮</button>

再来个巨常用的

v-click-outside 点击外部关闭 指令,这个指令在开发模态框(Modal)、下拉菜单(Dropdown)时非常有用。

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>
const vClickOutside = {
mounted(el, binding) {
// 定义点击处理函数
el.clickOutsideEvent = (event) => {
// 核心逻辑:
// 1. 点击的元素不是 el 本身
// 2. 点击的元素也不在 el 内部
if (!(el === event.target || el.contains(event.target))) {
// 执行传递进来的回调函数
binding.value(event)
}
}
// 添加全局点击监听
document.addEventListener('click', el.clickOutsideEvent)
},
unmounted(el) {
// 记得在组件卸载时移除监听,防止内存泄漏
document.removeEventListener('click', el.clickOutsideEvent)
}
}

const closeModal = () => {
console.log('点击了外部,关闭弹窗,哦哦牛批')
}
</script>

<template>
<!-- 点击这个 div 以外的区域会触发 closeModal -->
<div v-click-outside="closeModal" style="width: 200px; height: 200px; background: pink;">
我是弹窗,点我外面试试
</div>
</template>

注意在 unmounted 钩子中记得清理事件监听器,防止内存泄漏。

Vue模板插值

插值

文本

Vue.js 使用双大括号 {{ }} 来表示文本插值

这是数据绑定最常见的形式,会将内容作为纯文本处理,不会解析 HTML

1
<div>{{ message }}</div>

{{...}} 标签的内容将会被替代为对应组件实例中 message 属性的值,如果 message 属性的值发生了改变,{{...}} 标签内容也会更新。

如果不想改变标签的内容,可以通过使用 v-once 指令执行一次性地插值,当数据改变时,插值处的内容不会更新。

1
<span v-once>这个将不会改变: {{ message }}</span>

因为v-once 指令代表只渲染元素和组件一次

它也支持表达式,可以包含单个 JavaScript 表达式(注意:不能是语句或流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="app">
{{5+5}}<br>
{{ ok ? 'YES' : 'NO' }}<br>
{{ message.split('').reverse().join('') }}
<div v-bind:id="'list-' + id">菜鸟教程</div>
</div>

<script>
const app = {
data() {
return {
ok: true,
message: 'RUNOOB!!',
id: 1
}
}
}

Vue.createApp(app).mount('#app')
</script>

html

如果需要渲染真正的 HTML 内容,需要使用 v-html 指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 数据定义 -->
<script>
export default {
data() {
return {
rawHtml: '<span style="color: red;">这是红色文本</span>'
}
}
}
</script>

<!-- 模板使用 -->
<p>文本插值:{{ rawHtml }}</p> <!-- 输出纯文本:<span style="color: red;">这是红色文本</span> -->
<p>HTML 插值:<span v-html="rawHtml"></span></p> <!-- 渲染为红色文本 -->

永远不要使用 v-html 渲染用户输入的内容可能导致 XSS 攻击

只在可信内容上使用,永远不要用在用户提交的内容上

v-html 会覆盖元素内部的所有内容

属性绑定

Vue 数据驱动视图的核心机制之一就在于属性绑定,属性绑定让你能够用组件的响应式数据来动态控制 HTML 元素的属性值,实现数据与 DOM 属性的联动

数据变化 → 属性自动更新 → DOM 视图同步变化,这样无需手动操作 DOM。

HTML 属性不能使用双大括号插值,需要使用 v-bind 指令:

1
2
3
4
5
6
7
8
9
10
11
<!-- 基础用法 -->
<div v-bind:id="dynamicId"></div>

<!-- 缩写形式 -->
<div :id="dynamicId"></div>

<!-- 布尔属性 -->
<button :disabled="isDisabled">按钮</button> <!-- isDisabled 为 true 时添加 disabled 属性 -->

<!-- 动态属性名 -->
<div :[attributeName]="value"></div> <!-- attributeName 是变量,动态决定属性名 -->

对于布尔属性,常规值为 true 或 false,如果属性值为 null 或 undefined ,则该属性不会显示出来。

1
<button v-bind:disabled="isButtonDisabled">按钮</button>

常用场景

1
2
3
4
5
6
7
8
9
10
11
<!-- 绑定 class -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[activeClass, errorClass]"></div>

<!-- 绑定 style -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<div :style="[baseStyles, overridingStyles]"></div>

<!-- 绑定 href/src -->
<a :href="url">链接</a>
<img :src="imageUrl" alt="示例图片">

Vue声明式渲染

什么是声明式渲染

声明式编程(英语zhidao:Declarative programming)是一种编程范型,与命令式编程相对立。

它描述目标性质,让计算机明白目标,而非流程。声明式编程不用告诉电脑问题领域,从而避免随之而来的副作用。而指令式编程专则需要用算法来明确的指出每一步该怎么做。

简单来说,相对于命令式编程,声明式编程不注重实现过程而侧重于结果。

传统的命令式编程(如原生 JS 或 jQuery)关注的是 “怎么做”(获取 DOM -> 修改属性 -> 绑定事件 -> 手动更新)。 而 Vue 的声明式渲染关注的是 “要什么”(定义数据状态 -> 在模板中声明结果),中间的 DOM 操作由 Vue 帮你完成。

Vue 的声明式渲染是指通过简洁的模板语法来描述页面的结构和数据绑定关系,而不需要直接操作DOM。

这种方式使得开发者能够更专注于数据和页面结构的关系,而不必关心底层的DOM操作细节。

声明式渲染是 Vue 的核心概念之一,它的实现依赖于 Vue 的模板系统和响应式数据绑定机制。

Vue3 的声明式渲染通过使用模板、指令(如 v-if、v-for、v-bind 等)以及响应式数据来简化 UI 更新过程。

Vue 的声明式渲染让你只需要声明 UI 应该如何呈现,Vue 会根据数据的变化自动更新视图,当你改变数据时,视图会自动响应。

image-20251122195432953

而 Vue 的所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。

这意味着你不能使用箭头函数来定义一个生命周期方法,这是因为箭头函数绑定了父上下文(由于this的指向),因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

image-20251123202312729

挂载(初始化相关属性)

  • beforeCreate
  • created
  • beforeMount
  • mounted

更新(元素或组件的变更操作)

  • beforeUpdate
  • updated

销毁(销毁相关属性)

  • beforeDestroy
  • destroyed

条件渲染

Vue 通过 v-ifv-else-ifv-else 指令实现条件渲染,根据某个数据条件来决定是否渲染某个 DOM 元素。

上面也说了

  • v-if:真正的条件渲染。如果条件为假,元素根本不会被渲染到 DOM 中(物理消失)。
  • v-show:仅仅切换元素的 CSS display 属性(视觉消失,DOM 还在)。
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
<script setup>
import { ref } from 'vue'

// 定义状态
const isLogin = ref(false)
const showDetail = ref(true)

const toggleLogin = () => {
isLogin.value = !isLogin.value
}
</script>

<template>
<!-- 声明式:v-if -->
<!-- 只有当 isLogin 为 true 时,这个 div 才会被创建 -->
<div v-if="isLogin">
<p>欢迎回来,用户!</p>
</div>
<div v-else>
<p>请先登录。</p>
</div>

<button @click="toggleLogin">
{{ isLogin ? '退出' : '登录' }}
</button>

<hr />

<!-- 声明式:v-show -->
<!-- 元素始终在 DOM 中,只是通过 display:none 隐藏 -->
<div v-show="showDetail" style="background: #eee; padding: 10px;">
<p>这是详情内容,切换非常频繁,所以用 v-show 开销更小。</p>
</div>

<button @click="showDetail = !showDetail">切换详情显示</button>
</template>

列表渲染

在原生 JS 中,渲染列表通常需要写一个 for 循环,在循环体内创建 li 标签,填充内容,然后 append 到 ul 中。这非常繁琐且容易出错。

Vue 使用 v-for 指令,让你像写 JSON 一样直观地描述列表结构。

Vue 会根据数组的每一项渲染对应的 DOM 元素,并且在数组数据变化时,自动更新视图。

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
<script setup>
import { ref } from 'vue'

// 定义一个响应式数组
const todoList = ref([
{ id: 1, title: '学习 Vue3', done: true },
{ id: 2, title: '复习 JavaScript', done: false },
{ id: 3, title: '写个 Demo', done: false }
])

const addTodo = () => {
// 只需要操作数据数组,界面自动更新
todoList.value.push({
id: Date.now(),
title: '新任务',
done: false
})
}
</script>

<template>
<ul>
<!-- 声明式:v-for -->
<!-- 这里的语义是:为 todoList 中的每一项渲染一个 li -->
<!-- :key 是为了给每个节点唯一的身份证,帮助 Vue 高效更新 -->
<li v-for="(item, index) in todoList" :key="item.id">
<span>{{ index + 1 }}. </span>
<span :style="{ textDecoration: item.done ? 'line-through' : 'none' }">
{{ item.title }}
</span>
</li>
</ul>

<button @click="addTodo">添加任务</button>
</template>

当我们点击添加任务时,我们只操作了数组 (push)。我们没有手动创建新的 DOM 节点插入页面。Vue 监测到数组变了,自动根据模板生成新的 li

双向数据绑定

在原生 JS 中,我们需要监听 input 事件获取值,更新 JS 变量,如果 JS 变量变了,又要手动改 input.value。 Vue 通过 v-model 指令将这两步合二为一。

Vue 提供了 v-model指令来实现表单元素(如 <input>)和组件数据之间的双向绑定,这样,表单元素的值与数据模型保持同步,用户输入时会自动更新数据,数据变化时会自动更新视图。

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 username = ref('admin')
const description = ref('')
</script>

<template>
<div class="form-group">
<label>用户名:</label>
<!-- 声明式:v-model -->
<!-- 1. 输入框变化 -> 自动更新 username 变量 -->
<!-- 2. username 变量变化 -> 自动更新输入框的值 -->
<input v-model="username" type="text" />
</div>

<div class="preview">
<p>当前输入的用户名是:{{ username }}</p>
</div>

<!-- v-model 也适用于 textarea, select 等 -->
<textarea v-model="description" placeholder="个人简介"></textarea>
</template>

v-model 本质上是一个语法糖。 <input v-model="text">等价于 <input :value="text" @input="text = $event.target.value"> 声明式的好处在于,你不需要关心事件监听和值赋值的细节,只需要关注数据本身

Vue事件处理

Vue 使用 v-on(缩写为 @)来声明式地绑定事件。

这与原生 JS 的 onclick 最大的区别在于:Vue 的事件处理函数可以轻松访问组件的上下文,并且 Vue 提供了事件修饰符来声明式地处理 DOM 事件细节(如阻止冒泡、阻止默认行为)。

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>
import { ref } from 'vue'

const count = ref(0)

// 事件处理函数
const add = () => {
count.value++
}

// 带参数的处理函数
const say = (msg) => {
alert(msg)
}

// 表单提交
const handleSubmit = () => {
console.log('表单已提交,但页面不会刷新')
}
</script>

<template>
<!-- 1. 简单的内联事件 -->
<button @click="count++">点我 +1</button>
<p>当前计数:{{ count }}</p>

<!-- 2. 绑定方法 -->
<button @click="add">绑定函数 +1</button>

<!-- 3. 声明式修饰符 -->
<!-- .prevent 告诉 Vue:触发 submit 时自动调用 event.preventDefault() -->
<!-- 我们不需要在 handleSubmit 函数里写 e.preventDefault() -->
<form @submit.prevent="handleSubmit">
<button type="submit">提交表单</button>
</form>

<!-- .stop 阻止冒泡 -->
<div @click="say('父元素被点了')" style="padding: 20px; background: #ccc;">
父元素
<button @click.stop="say('只有子元素被点了')">阻止冒泡按钮</button>
</div>
</template>

使用 .prevent.stop 修饰符,体现了声明式编程的精髓:我们告诉 Vue “不要刷新页面”或“不要冒泡”,而不是自己去写代码实现这个逻辑。 这让我们的 JS 代码纯粹只关注业务逻辑,而不混杂 DOM 事件处理逻辑

Vue计算属性

计算属性是声明式渲染的高级形式。

有时候我们需要依赖现有的数据计算出一个新数据。如果不使用计算属性,我们可能需要在每个改变数据的地方手动重新计算,或者在模板里写复杂的逻辑。

  1. 自动追踪依赖:依赖的数据变了,它自动重新计算。
  2. 缓存:依赖的数据没变,它直接返回上一次的结果(性能更好)。
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
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 声明式:fullName 永远依赖于 firstName 和 lastName
// 我们不需要手动监听 firstName 的变化去更新 fullName
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})

// 场景2:购物车总价
const cart = ref([
{ name: 'Apple', price: 10, count: 2 },
{ name: 'Banana', price: 5, count: 5 }
])

// 只要 cart 里的 price 或 count 变了,totalPrice 会自动更新
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
</script>

<template>
<div>
<input v-model="firstName" />
<input v-model="lastName" />
<p>全名:{{ fullName }}</p>
</div>

<div>
<h3>购物车总价:{{ totalPrice }}</h3>
<button @click="cart[0].count++">增加苹果数量</button>
</div>
</template>

在命令式编程中,你可能需要在 inputonchange 事件里手动执行 span.innerText = first + last 而在 Vue 中,computed 定义了一个衍生状态。你只需要声明“总价等于单价乘以数量之和”,剩下的同步工作全交给 Vue。

这就是声明式的最高境界:描述数据之间的关系