Vue 基础

Vue note

reference: Vue 官方文档

核心功能:

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

单文件组件

*.vue 文件:将一个组件的逻辑 (JavaScript),模板 (HTML) 和样式 (CSS) 封装在同一个文件里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
<button @click="count++">Count is: {{ count }}</button>
</template>

<style scoped>
button {
font-weight: bold;
}
</style>

API 风格:选项式、组合式

创建应用

应用创建:

挂载容器(DOM 元素/ CSS 选择器) <——mount—— 应用实例(createapp 对象) <- 根组件 <- 多个子组件

应用实例包括 .config 对象进行应用配置

包括一些方法以注册应用范围内可用资源

应该在挂载前完成配置

可以存在多个应用实例

模板语法

Vue 使用基于 HTML 的模板语法,声明式地将组件实例绑定到 DOM 上

Vue 将模板编译为高度优化的 Javascript 代码

文本插值

最基本的数据绑定形式,使用双大括号语法

1
<span>Messege : {{msg}}</span>

双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

指令

指令以 v- 为前缀,表明它们是由 Vue 提供的特殊 attribute。

e.g.

1
2
<p>Using text interpolation: {{ rawHtml }}</p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>

image-20250227004523043

响应式绑定 attribute

v-bind 指令:

1
<div v-bind:id="dynamicId"></div>

v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

简写

1
<div :id="dynamicId"></div>

同名简写

如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:

1
2
3
4
5
<!-- 与 :id="id" 相同 -->
<div :id></div>

<!-- 这也同样有效 -->
<div v-bind:id></div>

布尔型 attribute

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

v-bind 在这种场景下的行为略有不同:

1
<button :disabled="isButtonDisabled">Button</button>

isButtonDisabled真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。

动态绑定多个值

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

1
2
3
4
5
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color:green'
}

通过不带参数的 v-bind,你可以将它们绑定到单个元素上:

1
<div v-bind="objectOfAttrs"></div>

使用 JavaScript 表达式

Vue 在所有的数据绑定中都支持完整的 JavaScript 表达式

1
2
3
4
5
6
7
{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

作用域为当前组件实例

在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:

  • 在文本插值中 (双大括号)
  • 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中

每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。

可以在绑定的表达式中使用一个组件暴露的方法:

1
2
3
<time :title="toTitleDate(date)" :datetime="date">
{{ formatDate(date) }}
</time>

[!TIP]

绑定在表达式中的方法在组件每次更新时都会被重新调用,因此应该产生任何副作用,比如改变数据或触发异步操作。

模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象

也可以自行在 app.config.globalProperties 上显式地添加它们,供所有的 Vue 表达式使用。

指令

指令是带有 v- 前缀的特殊 attribute

指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-forv-onv-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。

v-if 为例:

1
<p v-if="seen">Now you see me</p>

这里,v-if 指令会基于表达式 seen 的值的真假来移除/插入该 <p> 元素。

参数 arguments

某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。

1
2
3
4
<a v-bind:href="url"> ... </a>

<!-- 简写 -->
<a :href="url"> ... </a>

另一个例子是 v-on 指令,它将监听 DOM 事件:

1
2
3
4
<a v-on:click="doSomething"> ... </a>

<!-- 简写 -->
<a @click="doSomething"> ... </a>

这里的参数是要监听的事件名称

动态参数:同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:

1
2
3
4
<a v-bind:[attributeName]="url"> ... </a>

<!-- 简写 -->
<a :[attributeName]="url"> ... </a>

这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。

相似地,还可以将一个函数绑定到动态的事件名称上:

1
2
3
4
<a v-on:[eventName]="doSomething"> ... </a>

<!-- 简写 -->
<a @[eventName]="doSomething"> ... </a>

注意事项:

  • 动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。
  • 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。
  • 如果需要传入一个复杂的动态参数,推荐使用计算属性替换复杂的表达式
  • 当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写。单文件组件内的模板受此限制。

修饰符

修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。

指令语法图

响应式基础

声明响应式状态 :ref() 函数

1
2
3
import { ref } from 'vue'

const count = ref(0)

ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:

要在组件模板中访问 ref,从组件的 setup() 函数中声明并返回它们:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { ref } from 'vue'

export default {
// `setup` 是一个特殊的钩子,专门用于组合式 API。
setup() {
const count = ref(0)

// 将 ref 暴露给模板
return {
count
}
}
}
1
<div>{{ count }}</div>

注意,在模板中使用 ref 时,我们需要附加 .value。为了方便起见,当在模板中使用时,ref 会自动解包

也可以直接在事件监听器中改变一个 ref:

1
2
3
<button @click="count++">
{{ count }}
</button>

对于更复杂的逻辑,我们可以在同一作用域内声明更改 ref 的函数,并将它们作为方法与状态一起公开

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

export default {
setup() {
const count = ref(0)

function increment() {
// 在 JavaScript 中需要 .value
count.value++
}

// 不要忘记同时暴露 increment 函数
return {
count,
increment
}
}
}

然后,暴露的方法可以被用作事件监听器:

1
2
3
<button @click="increment">
{{ count }}
</button>

单文件组件(SFC)

setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup> 来大幅度地简化代码:

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

const count = ref(0)

function increment() {
count.value++
}
</script>

<template>
<button @click="increment">
{{ count }}
</button>
</template>

<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 > JavaScript 函数——它自然可以访问与它一起声明的所有内容。

为什么使用 ref?

响应式系统:当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。

在标准的 JavaScript 中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。

.value 属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:

1
2
3
4
5
6
7
8
9
10
11
const myRef = {
_value: 0,
get value() {
track()
return this._value
},
set value(newValue) {
this._value = newValue
trigger()
}
}

另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。

深层响应性

ref 可以持有任何类型的值,包括深层嵌套的对象、数组或者 JavaScript 内置的数据结构

Ref 会使它的值具有深层响应性。这意味着即使改变嵌套对象或数组时,变化也会被检测到

非原始值将通过 reactive() 转换为响应式代理

也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value 的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。

DOM 更新

当修改了响应式状态时,DOM 会被自动更新。

但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。

要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

1
2
3
4
5
6
7
import { nextTick } from 'vue'

async function increment() {
count.value++
await nextTick()
// 现在 DOM 已经更新了
}

reactive()

另一种声明响应式状态的方式

reactive() 将使对象本身具有响应性

响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。

reactive() 将深层地转换对象:当访问嵌套对象时,它们也会被 reactive() 包装。当 ref 的值是一个对象时,ref() 也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive() API 可以选择退出深层响应性。

值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的

只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本

为保证访问代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其本身(单例)

局限性

  1. 有限的值类型:只适用于对象,不能持有原始类型(string number boolean

  2. 不能替换对象

  3. 对解构操作不友好

ref 解包

一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。

只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。

与 reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包:

在模板渲染上下文中,只有顶级的 ref 属性才会被解包。

在下面的例子中,countobject 是顶级属性,但 object.id 不是:

1
2
const count = ref(0)
const object = { id: ref(1) }

计算属性

例子:

1
2
3
4
5
6
7
8
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
1
2
<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

使用计算属性:

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 { reactive, computed } from 'vue'

const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})

// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>

<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>

我们在这里定义了一个计算属性 publishedBooksMessagecomputed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref

计算属性相比于方法的好处:计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

计算属性默认只读,如果需要写,可以同时提供 getter 和 setter 来创建

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

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

const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>

如果需要,可以访问计算属性 getter 的第一个参数来获取计算属性返回的上一个值:

注意:

  1. Getter 不应有副作用:计算属性的 getter 应只做计算而没有任何其他的副作用。不要改变其他状态、在 getter 中做异步请求或者更改 DOM

  2. 避免直接修改计算属性值

Class 与 Style 绑定

Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

绑定 HTML class

可以给 :class 传递一个对象来动态切换 class

1
<div :class="{ active: isActive }"></div>

上面的语法表示 active 是否存在取决于数据属性 isActive真假值

可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。

1
2
3
4
5
6
7
<div
class="static"
:class="{ active: isActive, 'text-danger': hasError }"
></div>

<!-- 渲染结果可能是 -->
<div class="static active"></div>

isActive 或者 hasError 改变时,class 列表会随之更新。

绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象;

也可以绑定一个返回对象的计算属性

可以给 :class 绑定一个数组来渲染多个 CSS class

1
2
const activeClass = ref('active')
const errorClass = ref('text-danger')
1
<div :class="[activeClass, errorClass]"></div>

结果:

1
<div class="active text-danger"></div>

也可以在数组中嵌套对象

1
<div :class="[{ [activeClass]: isActive }, errorClass]"></div>

组件也可以绑定

绑定内联样式

绑定对象

1
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

直接绑定一个样式对象:

1
2
3
4
const styleObject = reactive({
color: 'red',
fontSize: '30px'
})
1
<div :style="styleObject"></div>

如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性

绑定数组

我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上

条件渲染

v-if

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

类似的,v-else v-else-if

v-show

一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

列表渲染

v-for

我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

1
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
1
2
3
<li v-for="item in items">
{{ item.message }}
</li>

v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

1
2
const parentMessage = ref('Parent')
const items = ref([{ message: 'Foo' }, { message: 'Bar' }])
1
2
3
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域

也可以使用 of 作为分隔符来替代 in

你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.values() 的返回值来决定。

可以通过提供第二个参数表示属性名 (例如 key) 第三个参数表示位置索引

1
2
3
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>

v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

1
<span v-for="n in 10">{{ n }}</span>

注意此处 n 的初值是从 1 开始而非 0

也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。

事件处理

监听事件

v-on 指令

用法:v-on:click="handler"@click="handler"

事件处理器 (handler) 的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

各种修饰符

表单输入绑定

v-model

将表单输入框的内容同步给 JavaScript 中相应的变量

基本用法

文本:

1
2
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

多行文本:

1
2
3
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

复选框:

1
2
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

也可以将多个复选框绑定到同一个数组或集合的值

侦听器

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:

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

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>

<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

1
2
3
4
5
6
7
8
9
const obj = reactive({ count: 0 })

watch(obj, (newValue, oldValue) => {
// 在嵌套的属性变更时触发
// 注意:`newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
})

obj.count++

相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:

1
2
3
4
5
6
watch(
() => state.someObject,
() => {
// 仅当 state.someObject 被替换时触发
}
)

也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:

1
2
3
4
5
6
7
8
watch(
() => state.someObject,
(newValue, oldValue) => {
// 注意:`newValue` 此处和 `oldValue` 是相等的
// *除非* state.someObject 被整个替换了
},
{ deep: true }
)

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。

我们可以通过传入 immediate: true 选项来强制侦听器的回调立即执行:

1
2
3
4
5
6
7
watch(
source,
(newValue, oldValue) => {
// 立即执行,且当 `source` 改变时再次执行
},
{ immediate: true }
)

如果希望回调只在源变化时触发一次,请使用 once: true 选项。

1
2
3
4
5
6
7
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true }
)

模板引用

直接访问底层 DOM 元素:使用特殊的 ref attribute

1
<input ref="input">

它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。

访问模板引用

辅助函数 useTemplateRef()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 第一个参数必须与模板中的 ref 值匹配
const input = useTemplateRef('my-input')

onMounted(() => {
input.value.focus()
})
</script>

<template>
<input ref="my-input" />
</template>

v-for 中的模板引用

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

应该注意的是,ref 数组并不保证与源数组相同的顺序。

组件基础

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成一个层层嵌套的树状结构:

组件树

Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。

定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

1
2
3
4
5
6
7
8
9
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>

当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from 'vue'

export default {
setup() {
const count = ref(0)
return { count }
},
template: `
<button @click="count++">
You clicked me {{ count }} times.
</button>`
// 也可以针对一个 DOM 内联模板:
// template: '#my-template-element'
}

这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源。

上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。

使用组件

要使用一个子组件,我们需要在父组件中导入它。

1
2
3
4
5
6
7
8
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
<h1>Here is a child component!</h1>
<ButtonCounter />
</template>

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。

每当你使用一个组件,就创建了一个新的实例

传递 props

向组件中传递数据会使用到 props

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps 宏:

1
2
3
4
5
6
7
8
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
<h4>{{ title }}</h4>
</template>

defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

1
2
3
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

监听事件

组件实例提供了一个自定义事件系统。父组件可以通过 v-on@ 来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:

1
2
3
4
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>

子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件

1
2
3
4
5
6
7
<!-- BlogPost.vue, 省略了 <script> -->
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>

我们可以通过 defineEmits 宏来声明需要抛出的事件:

这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。

defineProps 类似,defineEmits 仅可用于 <script setup> 之中,并且不需要导入,它返回一个等同于 $emit 方法的 emit 函数。它可以被用于在组件的 <script setup> 中抛出事件,因为此处无法直接访问 $emit

通过插槽分配内容

一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:

这可以通过 Vue 的自定义 <slot> 元素来实现。

我们使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在这里。

动态组件

有些场景会需要在两个组件间来回切换,比如 Tab 界面:

在演练场中查看示例

上面的例子是通过 Vue 的 <component> 元素和特殊的 is attribute 实现的:

在上面的例子中,被传给 :is 的值可以是以下几种:

  • 被注册的组件名
  • 导入的组件对象

当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。

生命周期

组件生命周期图示


Vue 基础
https://www.jcjovo.top/2025/02/27/vue/
作者
jcjovo
发布于
2025年2月27日
许可协议