快速开始
基础
模板语法
attribute
{{}}
不能在 HTML attributes 中使用。想要响应式的绑定一个 attribute,应该使用v-bind
指令是:
1 | <div v-bind:id="dynamicId"></div> |
因为v-bind
非常常用,我们提供了特定的简写语法:
1 | <div :id="dynamicId"></div> |
使用 js 表达式
实际上,vue 在所有的数据绑定中都支持完整的 js 表达式。
每个帮顶仅支持单一表达式,也就是一段能够被求值的 js 代码。一个简单的判断是是否可以合法的写在return
后面。
绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
受限的全局访问:
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 Math
和 Date
。
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window
上的属性。然而,你也可以自行在 app.config.globalProperties
上显式地添加它们,供所有的 Vue 表达式使用。
指令
指令是带有v-
前缀的特殊 attribute,vue 提供了许多内置指令,包括上面提到的v-bind
。
指令 attribute 的期望值是一个 js 表达式(除了v-for
、v-on
、v-slot
这几个少数的例外)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。
以v-if
为例:
1 | <p v-if="seen">Now you see me</p> |
这里,v-if
指令会基于表达式 seen
的值的真假来移除/插入该 <p>
元素。
响应式基础
reactive()
我们可以使用 reactive()
函数创建一个响应式对象或数组:
1 | import { reactive } from "vue" |
<script setup>
要在组件模板中使用响应式状态,需要在 setup()
函数中定义并返回。
1 | <script> |
在 setup() 函数中手动暴露大量的状态和方法非常繁琐。幸运的是,我们可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用 <script setup>
来大幅度地简化代码。
1 | <script setup> |
<script setup>
中的顶层的导入和变量声明可在同一组件的模板中直接使用。你可以理解为模板中的表达式和 <script setup>
中的代码处在同一个作用域中。
DOM 更新时机
当你更改响应式状态后,DOM 会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的 “下个时机” 以确保无论你进行了多少次状态更改,每个组件都只更新一次。
若要等待一个状态改变后的 DOM 更新完成,你可以使用 nextTick() 这个全局 API:
1 | import { nextTick } from "vue" |
深层响应性
在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
1 | import { reactive } from "vue" |
你也可以直接创建一个浅层响应式对象。它们仅在顶层具有响应性,一般仅在某些特殊场景中需要。
响应式代理 vs 原始对象
值得注意的是,reactive() 返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
1 | const raw = {} |
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 vue 的响应式系统的最佳实践是 仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用reactive()
总是返回同样的代理对象,而对一个已存在的代理对象调用reactive()
会返回其本身:
1 | // 在同一个对象上调用 reactive() 会返回相同的代理 |
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
1 | const proxy = reactive({}) |
reactive()
的局限性
因为 js 没有可以作用于所有值类型的“引用”机制。所以reactive()
API 有两条限制:
- 仅对对象类型有效(对象、数组和
Map
、Set
这样的集合类型),而对string
、number
和boolean
这样的 原始类型 无效。 - 必须始终保持对响应式对象的相同引用。不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失:
1 | let state = reactive({ count: 0 }) |
同时这也意味着当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性:
1 | const state = reactive({ count: 0 }) |
ref()
js 没有可以作用于所有值类型的“引用”机制,为此,vue 提供了一个ref()
方法来允许我们创建可以使用任何值类型的响应式 ref:
1 | import { ref } from "vue" |
ref()
将传入参数的值包装为一个带.value
属性的 ref 对象:
1 | const count: Ref<number> = ref(0) |
和响应式对象的属性类似,ref 的 .value
属性也是响应式的。同时,当值为对象类型时,会用 reactive()
自动转换它的 .value
。
一个包含对象类型值的 ref 可以响应式地替换整个对象:
1 | const objectRef = ref({ count: 0 }) |
ref 被传递给函数或是从一般对象上被解构时,不会丢失响应性:
1 | const obj = { |
简言之,ref()
让我们能创造一种对任意值的 “引用”,并能够在不丢失响应性的前提下传递这些引用。这个功能很重要,因为它经常用于将逻辑提取到 组合函数 中。
ref 在模板中的解包
当 ref 在模板中作为顶层属性被访问时,它们会自动“解包”,所以不需要使用.value
,示例:
1 | <script setup> |
请注意,仅当 ref 是模板渲染上下文的顶层属性时才适用自动“解包”。 例如:
1 | const object = { foo: ref(1) } |
下面的表达式将不会像预期的那样工作:
1 | { |
因为此时 ref 所在的上下文是object
而不是模板。我们可以将foo
提取出来,这样 ref 的上下文就是模板了:
1 | const { foo } = object |
1 | { |
需要注意的是,如果是下面这种情况,直接渲染,不参与计算,则也会被自动解包:
1 | { |
ref 在响应式对象中的解包
当一个ref
被嵌套在一个响应式对象中,作为属性被访问或更改时,它会自动解包,因此会表现的和一般属性一样:
1 | const count = ref(0) |
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
1 | const otherCount = ref(2) |
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包,当其作为浅层响应式对象的属性被访问时不会解包。
数组和集合类型的 ref 解包
跟响应式对象不同,当 ref 作为响应式数组或像 Map
这种原生集合类型的元素被访问时,不会进行解包。
1 | const books = reactive([ref("Vue 3 Guide")]) |
计算属性
1 | <script lang="ts" setup> |
我们在这里定义了一个计算属性 publishedBooksMessage
。computed()
方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value
访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value
。
vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage
依赖于 author.books
,所以当 author.books
改变时,任何依赖于 publishedBooksMessage
的绑定都会同时更新。
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
1 | <p>{{ calculateBooksMessage() }}</p> |
1 | // 组件中 |
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now()
并不是一个响应式依赖:
1 | const now = computed(() => Date.now()) |
相比之下,方法调用总是会在重渲染发生时再次执行函数。
为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list
,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list
。没有缓存的话,我们会重复执行非常多次 list
的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。
可写计算属性
计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:
1 | <script setup> |
现在当你再运行 fullName.value = 'John Doe'
时,setter 会被调用而 firstName
和 lastName
会随之更新。
最佳实践
Getter 不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用监听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
Class 与 Style 绑定
因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定 HTML class
绑定对象
:class
是 v-bind:class
的缩写。
1 | <div :class="{ active: isActive }"></div> |
也可以直接绑定一个对象:
1 | const classObject = reactive({ |
1 | <div :class="classObject"></div> |
也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
1 | const isActive = ref(true) |
1 | <div :class="classObject"></div> |
绑定数组
我们可以给 :class
绑定一个数组来渲染多个 CSS class:
1 | const activeClass = ref("active") |
1 | <div :class="[activeClass, errorClass]"></div> |
渲染的结果是:
1 | <div class="active text-danger"></div> |
如果你也想在数组中有条件地渲染某个 class,你可以使用三元表达式:
1 | <div :class="[isActive ? activeClass : '', errorClass]"></div> |
然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象:
1 | <div :class="[{ active: isActive }, errorClass]"></div> |
在组件上使用
1 | <!-- 子组件模板 --> |
绑定内联样式
绑定对象
1 | const activeColor = ref("red") |
1 | <div :style="{ 'font-size': fontSize + 'px' }"></div> |
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
1 | const styleObject = reactive({ |
1 | <div :style="styleObject"></div> |
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
绑定数组
我们还可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
1 | <div :style="[baseStyles, overridingStyles]"></div> |
自动前缀
样式多值
条件渲染
v-if
1 | <div v-if="type === 'A'">A</div> |
一个 v-else
元素必须跟在一个 v-if
或者 v-else-if
元素后面,否则它将不会被识别。
v-if
、v-else
和 v-else-if
也可以在 <template>
上使用。
1 | <template v-if="ok"> |
v-show
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
1 | <h1 v-show="ok">Hello!</h1> |
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
v-if
和 v-show
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show
较好;如果在运行时绑定条件很少改变,则 v-if
会更合适。
列表渲染
v-for
1 | const items = ref([{ message: "Foo" }, { message: "Bar" }]) |
1 | <li v-for="item in items">{{ item.message }}</li> |
你也可以使用 of
作为分隔符来替代 in
,这更接近 JavaScript 的迭代器语法:
1 | <div v-for="item of items"></div> |
v-for
与对象
你也可以使用 v-for
来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys()
的返回值来决定。
1 | const myObject = reactive({ |
1 | <ul> |
在 v-for 里使用范围值
v-for
可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次。
1 | <span v-for="n in 10">{{ n }}</span> |
注意此处 n
的初值是从 1
开始而非 0
。
<template>
上的 v-for
与模板上的 v-if
类似,你也可以在 <template>
标签上使用 v-for
来渲染一个包含多个元素的块。例如:
1 | <ul> |
v-for
和 v-if
警告:
同时使用
v-if
和v-for
是不推荐的,因为这样二者的优先级不明显。请查看风格指南获得更多信息。
当它们同时存在于一个节点上时,v-if
比 v-for
的优先级更高。这意味着 v-if
的条件将无法访问到 v-for
作用域内定义的变量别名:
1 | <!-- |
在外新包装一层 <template>
再在其上使用 v-for
可以解决这个问题 (这也更加明显易读):
1 | <template v-for="todo in todos"> |
通过 key 管理状态
vue 默认按照“就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况。
为了给 vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key
attribute:
1 | <div v-for="item in items" :key="item.id"> |
当你使用 <template v-for>
时,key
应该被放置在这个 <template>
容器上:
1 | <template v-for="todo in todos" :key="todo.name"> |
注意:key
在这里是一个通过 v-bind
绑定的特殊 attribute。请不要和在 v-for
中使用对象里所提到的对象属性名相混淆。
推荐在任何可行的时候为 v-for
提供一个 key
attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。
key
绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for
的 key。关于 key
attribute 的更多用途细节,请参阅 key
API 文档。
组件上使用v-for
我们可以直接在组件上使用 v-for
,和在一般的元素上使用没有区别 (别忘记提供一个 key
):
1 | <MyComponent v-for="item in items" :key="item.id" /> |
但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,我们还需要传递 props:
1 | <MyComponent v-for="(item, index) in items" :item="item" :index="index" :key="item.id" /> |
不自动将 item
注入组件的原因是,这会使组件与 v-for
的工作方式紧密耦合。明确其数据的来源可以使组件在其他情况下重用。
数组变化侦测
变更方法
vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
替换一个数组
变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter()
,concat()
和 slice()
,这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:
1 | // `items` 是一个数组的 ref |
你可能认为这将导致 vue 丢弃现有的 DOM 并重新渲染整个列表——幸运的是,情况并非如此。vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。
展示过滤或排序后的结果
有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。
举例:
1 | const numbers = ref([1, 2, 3, 4, 5]) |
1 | <li v-for="n in evenNumbers">{{ n }}</li> |
在计算属性不可行的情况下 (例如在多层嵌套的 v-for
循环中),你可以使用以下方法:
1 | const sets = ref([ |
1 | <ul v-for="numbers in sets"> |
在计算属性中使用 reverse()
和 sort()
的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:
1 | - return numbers.reverse() |
事件处理
监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"
或 @click="handler"
。
事件处理器的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
内联事件处理器
内联事件处理器通常用于简单场景,例如:
1 | const count = ref(0) |
1 | <button @click="count++">Add 1</button> |
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
举例:
1 | const name = ref("Vue.js") |
1 | <!-- `greet` 是上面定义过的方法名 --> |
方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target.tagName
访问到该 DOM 元素。
方法与内联事件判断
模板编译器会通过检查 v-on
的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo
、foo.bar
和 foo['bar']
会被视为方法事件处理器,而 foo()
和 count++
会被视为内联事件处理器。
在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
1 | function say(message) { |
1 | <button @click="say('hello')">Say hello</button> <button @click="say('bye')">Say bye</button> |
在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
1 | <!-- 使用特殊的 $event 变量 --> |
1 | function warn(message, event) { |
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
1 | <!-- 单击事件将停止传递 --> |
使用修饰符时需要注意调用顺序,因为相关代码是以相同的顺序生成的。因此使用 @click.prevent.self
会阻止元素及其子元素的所有点击事件的默认行为,而 @click.self.prevent
则只会阻止对元素本身的点击事件的默认行为。
.capture
、.once
和 .passive
修饰符与原生 addEventListener
事件相对应:
1 | <!-- 添加事件监听器时,使用 `capture` 捕获模式 --> |
.passive
修饰符一般用于触摸事件的监听器,可以用来改善移动端设备的滚屏性能。
请勿同时使用 .passive
和 .prevent
,因为 .passive
已经向浏览器表明了你不想阻止事件的默认行为。如果你这么做了,则 .prevent
会被忽略,并且浏览器会抛出警告。
按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on
或 @
监听按键事件时添加按键修饰符。
1 | <!-- 仅在 `key` 为 `Enter` 时调用 `submit` --> |
你可以直接使用 KeyboardEvent.key
暴露的按键名称作为修饰符,但需要转为 kebab-case 形式。
1 | <input @keyup.page-down="onPageDown" /> |
在上面的例子中,仅会在 $event.key
为 'PageDown'
时调用事件处理。
按键别名
Vue 为一些常用的按键提供了别名:
.enter
.tab
.delete
(捕获“Delete”和“Backspace”两个按键).esc
.space
.up
.down
.left
.right
系统按键修饰符
你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
.ctrl
.alt
.shift
.meta
在 Mac 键盘上,meta 是 Command 键 (⌘)。在 Windows 键盘上,meta 键是 Windows 键 (⊞)。在 Sun 微机系统键盘上,meta 是钻石键 (◆)。在某些键盘上,特别是 MIT 和 Lisp 机器的键盘及其后代版本的键盘,如 Knight 键盘,space-cadet 键盘,meta 都被标记为“META”。在 Symbolics 键盘上,meta 也被标识为“META”或“Meta”。
1 | <!-- Alt + Enter --> |
请注意,系统按键修饰符和常规按键不同。与 keyup
事件一起使用时,该按键必须在事件发出时处于按下状态。换句话说,keyup.ctrl
只会在你仍然按住 ctrl
但松开了另一个键时被触发。若你单独松开 ctrl
键将不会触发。
.exact
修饰符
.exact
修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。
1 | <!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 --> |
鼠标按键修饰符
.left
.right
.middle
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
表单输入与绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
1 | <input |
v-model
指令帮我们简化了这一步骤:
1 | <input v-model="text"> |
注意:
v-model
会忽略任何表单元素上初始的value
、checked
或selected
attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API 来声明该初始值。
值绑定
有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind
来实现。此外,使用 v-bind
还使我们可以将选项值绑定为非字符串的数据类型。
复选框
1 | <input type="checkbox" v-model="toggle" true-value="yes" false-value="no" /> |
true-value
和 false-value
是 Vue 特有的 attributes,仅支持和 v-model
配套使用。这里 toggle
属性的值会在选中时被设为 'yes'
,取消选择时设为 'no'
。你同样可以通过 v-bind
将其绑定为其他动态值:
1 | <input type="checkbox" v-model="toggle" :true-value="dynamicTrueValue" :false-value="dynamicFalseValue" /> |
提示:
true-value
和false-value
attributes 不会影响value
attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。
修饰符
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
1 | <!-- 在 "change" 事件后同步更新而不是 "input" --> |
.number
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
1 | <input v-model.number="age" /> |
如果该值无法被 parseFloat()
处理,那么将返回原始值。
number
修饰符会在输入框有 type="number"
时自动启用。
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
1 | <input v-model.trim="msg" /> |
组件上的v-model
HTML 的内置表单输入类型并不总能满足所有需求。幸运的是,我们可以使用 Vue 构建具有自定义行为的可复用输入组件,并且这些输入组件也支持 v-model
!要了解更多关于此的内容,请在组件指引中阅读配合 v-model
使用。
生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
侦听器
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
侦听器 和 计算属性
有“副作用”,使用侦听器;没有“副作用”,使用计算属性。
watch()
在组合式 API 中,我们可以使用 watch
函数在每次响应式状态发生变化时触发回调函数:
1 | <script setup> |
watch
的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:
1 | const x = ref(0) |
注意,你不能直接侦听响应式对象的属性值,例如:
1 | const obj = reactive({ count: 0 }) |
这里需要用一个返回该属性的 getter 函数:
1 | // 提供一个 getter 函数 |
深层侦听器
直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
1 | const obj = reactive({ count: 0 }) |
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
1 | watch( |
你也可以给上面这个例子显式地加上 deep
选项,强制转成深层侦听器:
1 | watch( |
谨慎使用:
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
1 | watch( |
watchEffect()
下面的例子中,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
1 | const todoId = ref(1) |
侦听的数据源是todoId
,而回调中也使用到了todoId
,这种情况是很常见的。
我们可以用 watchEffect
函数 来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
1 | watchEffect(async () => { |
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
提示:
watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个await
正常工作前访问到的属性才会被追踪。
watch
和 watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
回调的触发时机
当你更改了响应的状态,它可能会同时触发 vue 组件更新和侦听器回调。
默认情况下,用户创建的侦听器回调,都会在 vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 vue 更新的状态。
如果想在侦听器回调中能访问被 vue 更新之后的 DOM,你需要指明flush 'post'
选项:
1 | watch(source, callback, { |
后置刷新的watchEffect()
有个更方便的别名watchPostEffect()
:
1 | import { watchPostEffect } from "vue" |
停止侦听器
在 setup()
或 <script setup>
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:
1 | <script setup> |
要手动停止一个侦听器,请调用 watch
或 watchEffect
返回的函数:
1 | const unwatch = watchEffect(() => {}) |
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
1 | // 需要异步请求得到的数据 |
模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
1 | <input ref="input"> |
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key
类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
访问模板引用
为了通过组合式 API 获得该模板引用,我们需要声明一个同名的 ref:
1 | <script setup> |
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input
,在初次渲染时会是 null
。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null
的情况:
1 | watchEffect(() => { |
v-for
中的模板引用
当在 v-for
中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
1 | <script setup> |
应该注意的是,ref 数组并不保证与源数组相同的顺序。
函数模板引用
除了使用字符串值作名字,ref
attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
1 | <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }" /> |
注意我们这里需要使用动态的 :ref
绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el
参数会是 null
。你当然也可以绑定一个组件方法而不是内联函数。
组件上的 ref
模板引用也可以被用在一个子组件上。这种情况下引用中获得的值是组件实例:
1 | <script setup> |
如果一个子组件使用的是选项式 API 或没有使用 <script setup>
,被引用的组件实例和该子组件的 this
完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
有一个例外的情况,使用了 <script setup>
的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup>
的子组件中的任何东西,除非子组件在其中通过 defineExpose
宏显式暴露:
1 | <script setup> |
当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number }
(ref 都会自动解包,和一般的实例一样)。
组件基础
传递 props
props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps
宏:
1 | <!-- BlogPost.vue --> |
defineProps
是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props:
1 | const props = defineProps(["title"]) |
监听事件
父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
1 | <BlogPost ... @enlarge-text="postFontSize += 0.1" /> |
子组件可以通过调用内置的 $emit
方法,通过传入事名称来抛出一个事件:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
因为有了 @enlarge-text="postFontSize += 0.1"
的监听,父组件会接收这一事件,从而更新 postFontSize
的值。
我们可以通过 defineEmits
宏来声明需要抛出的事件:
1 | <!-- BlogPost.vue --> |
这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
和 defineProps
类似,defineEmits
仅可用于 <script setup>
之中,并且不需要导入,它返回一个等同于 $emit
方法的 emit
函数。它可以被用于在组件的 <script setup>
中抛出事件,因为此处无法直接访问 $emit
:
1 | <script setup> |
深入组件
注册
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
全局注册
我们可以使用 Vue 应用实例的 app.component()
方法,让组件在当前 Vue 应用中全局可用。
1 | import MyComponent from "./App.vue" |
app.component()
方法可以被链式调用:
1 | app.component("ComponentA", ComponentA).component("ComponentB", ComponentB).component("ComponentC", ComponentC) |
全局注册的组件可以在此应用的任意组件的模板中使用。并且相互可以在彼此内部使用。
局部注册
局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
组件名格式
在 SFC 中,推荐为子组件使用PascalCase
的标签名,以此来和原声的 HTML 元素作区分。
但是,PascalCase 的标签名在 DOM 模板中是不可用的,详情参见 DOM 模板解析注意事项,在这种情况下,需要使用 kebab-case
形式。
什么是 DOM 模板?就是直接写在 DOM 中的模板,会被浏览器直接解析:
1 |
|
<my-component></my-component>
就是 DOM 模板。
props
props 声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute (关于透传 attribute,我们会在专门的章节中讨论)。
1 | <script setup> |
如果使用了 ts,也可以这么声明:
1 | <script setup lang="ts"> |
这被称之为“基于类型的声明”。感觉怪怪的。
当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults
编译器宏解决:
1 | export interface Props { |
传递 prop 细节
prop 名字格式
prop 名字使用 camelCase 形式:
1 | defineProps({ |
然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格,使用 kebab-case 形式:
1 | <MyComponent greeting-message="hello" /> |
静态 和 动态 prop
静态:
1 | <BlogPost title="My journey with Vue" /> |
动态绑定:
1 | <!-- 根据一个变量的值动态传入 --> |
传递不同的值类型
不仅仅是字符串,实际上任何类型的值都可以作为 props 的值被传递。
Number:
1 | <!-- 虽然 `42` 是个常量,我们还是需要使用 v-bind --> |
Boolean:
1 | <!-- 仅写上 prop 但不传值,会隐式转换为 `true` --> |
Array:
1 | <!-- 虽然这个数组是个常量,我们还是需要使用 v-bind --> |
Object:
1 | <!-- 虽然这个对象字面量是个常量,我们还是需要使用 v-bind --> |
使用一个对象绑定多个 prop
如果你想要将一个对象的所有属性都当作 props 传入,你可以使用没有参数的 v-bind
,即只使用 v-bind
而非 :prop-name
。例如,这里有一个 post
对象:
1 | const post = { |
1 | <BlogPost v-bind="post" /> |
等价于:
1 | <BlogPost :id="post.id" :title="post.title" /> |
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告。
更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变。
组件事件
触发与监听事件
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件:
1 | <!-- MyComponent --> |
父组件监听事件:
1 | <MyButton @increase-by="n => (count += n)" /> |
同样,组件的事件监听器也支持 .once
修饰符:
1 | <MyComponent @some-event.once="callback" /> |
像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
提示:
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
声明触发的事件
组件可以显式地通过 defineEmits()
宏来声明它要触发的事件:
1 | <script setup lang="ts"> |
我们在 <template>
中使用的 $emit
方法不能在组件的 <script setup>
部分中使用,但 defineEmits()
会返回一个相同作用的函数供我们使用:
1 | <script setup> |
组件 v-model
v-model
可以在组件上使用以实现双向绑定。
首先让我们回忆一下 v-model
在原生元素上的用法:
1 | <input v-model="searchText" /> |
模板编译器会对 v-model
进行冗长的等价展开。因此上面的代码其实等价于下面这段:
1 | <input :value="searchText" @input="searchText = $event.target.value" /> |
而当使用在一个组件上时,v-model
会被展开为如下的形式:
1 | <CustomInput :modelValue="searchText" @update:modelValue="newValue => (searchText = newValue)" /> |
所以,<CustomInput>
组件内部需要做两件事:
- 将内部原生
<input>
元素的value
attribute 绑定到modelValue
prop - 当原生的
input
事件触发时,触发一个携带了新值的update:modelValue
自定义事件
1 | <!-- CustomInput.vue --> |
现在 v-model
可以在这个组件上正常工作了:
1 | <CustomInput v-model="searchText" /> |
另一种在组件内实现 v-model
的方式是使用一个可写的,同时具有 getter 和 setter 的 computed
属性。get
方法需返回 modelValue
prop,而 set
方法需触发相应的事件:
1 | <!-- CustomInput.vue --> |
v-model
的参数
默认情况下,v-model
在组件上都是使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。我们可以通过给 v-model
指定一个参数来更改这些名字:
1 | <MyComponent v-model:title="bookTitle" /> |
在这个例子中,子组件应声明一个 title
prop,并通过触发 update:title
事件更新父组件值:
1 | <!-- MyComponent.vue --> |
多个v-model
绑定
1 | <UserName v-model:first-name="first" v-model:last-name="last" /> |
1 | <script setup> |
处理v-model
修饰符
在学习输入绑定时,我们知道了 v-model
有一些内置的修饰符,例如 .trim
,.number
和 .lazy
。在某些场景下,你可能想要一个自定义组件的 v-model
支持自定义的修饰符。
我们来创建一个自定义的修饰符 capitalize
,它会自动将 v-model
绑定输入的字符串值第一个字母转为大写:
1 | <MyComponent v-model.capitalize="myText" /> |
组件的 v-model
上所添加的修饰符,可以通过 modelModifiers
prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers
这个 prop,它的默认值是一个空对象:
1 | <script setup> |
注意这里组件的 modelModifiers
prop 包含了 capitalize
且其值为 true
,因为它在模板中的 v-model
绑定 v-model.capitalize="myText"
上被使用了。
有了这个 prop,我们就可以检查 modelModifiers
对象的键,并编写一个处理函数来改变抛出的值。在下面的代码里,我们就是在每次 <input />
元素触发 input
事件时将值的首字母大写:
1 | <script setup> |
对于又有参数又有修饰符的 v-model
绑定,生成的 prop 名将是 arg + "Modifiers"
。举例来说:
1 | <MyComponent v-model:title.capitalize="myText"> |
相应的声明应该是:
1 | const props = defineProps(["title", "titleModifiers"]) |
透传 attributes
attributes 继承
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton>
组件,它的模板长这样:
1 | <!-- <MyButton> 的模板 --> |
一个父组件使用了这个组件,并且传入了 class
:
1 | <MyButton class="large" /> |
最后渲染出的 DOM 结果是:
1 | <button class="large">click me</button> |
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
对 class
和 style
的合并
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton>
组件的模板改成这样:
1 | <!-- <MyButton> 的模板 --> |
则最后渲染出的 DOM 结果会变成:
1 | <button class="btn large">click me</button> |
v-on
监听器继承
同样的规则也适用于 v-on
事件监听器:
1 | <MyButton @click="onClick" /> |
click
监听器会被添加到 <MyButton>
的根元素,即那个原生的 <button>
元素之上。当原生的 <button>
被点击,会触发父组件的 onClick
方法。同样的,如果原生 button
元素自身也通过 v-on
绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
深层组件继承
有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>
,让它在根节点上渲染 <BaseButton>
:
1 | <!-- <MyButton/> 的模板,只是渲染另一个组件 --> |
此时 <MyButton>
接收的透传 attribute 会直接继续传给 <BaseButton>
。
请注意:
- 透传的 attribute 不会包含
<MyButton>
上声明过的 props 或是针对emits
声明事件的v-on
侦听函数,换句话说,声明过的 props 和侦听函数被<MyButton>
“消费”了。 - 透传的 attribute 若符合声明,也可以作为 props 传入
<BaseButton>
。
禁用 attributes 继承
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
。
如果你使用了 <script setup>
,你需要一个额外的 <script>
块来书写这个选项声明:
1 | <script> |
最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs
选项为 false
,你可以完全控制透传进来的 attribute 被如何使用。
这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问到。
1 | <span>Fallthrough attribute: {{ $attrs }}</span> |
这个 $attrs
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-on
监听器等等。
有几点需要注意:
- 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像
foo-bar
这样的一个 attribute 需要通过$attrs['foo-bar']
来访问。 - 像
@click
这样的一个v-on
事件监听器将在此对象下被暴露为一个函数$attrs.onClick
。
1 | <div class="btn-wrapper"> |
小提示:没有参数的 v-bind
会将一个对象的所有属性都作为 attribute 应用到目标元素上。
多根节点的 attributes 继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs
没有被显式绑定,将会抛出一个运行时警告。
1 | <CustomLayout id="custom-layout" @click="changeValue" /> |
如果 <CustomLayout>
有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
1 | <header>...</header> |
如果 $attrs
被显式绑定,则不会有警告:
1 | <header>...</header> |
在 JavaScript 中访问透传 Attributes
如果需要,你可以在 <script setup>
中使用 useAttrs()
API 来访问一个组件的所有透传 attribute:
1 | <script setup> |
需要注意的是,虽然这里的 attrs
对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用声明周期函数 onUpdated()
使得在每次更新时结合最新的 attrs
执行副作用。
插槽 slots
依靠 props 传值,还是不够,如果要传递模板内容,则需要使用插槽 slots。
1 | <FancyButton> |
<FancyButton>
模板是这样的:
1 | <button class="fancy-btn"> |
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
1 | <button class="fancy-btn">Click me!</button> |
多个插槽
如果有多个插槽,需要给插槽命名。
1 | <div class="container"> |
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令,v-slot
有对应的简写 #
:
1 | <BaseLayout> |
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
1 | <BaseLayout> |
向插槽传参
Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。
子组件定义插槽,父组件中使用子组件的时候,定义插槽中的内容。
插槽中内容是在父组件中定义的,所以插槽中只能访问父组件的作用域,但是如果插槽中需要使用到子组件作用域中的数据,怎么办?
子组件在定义插槽的时候,将需要使用到的数据传入插槽,这样,父组件在定义插槽中的内容时,就能使用传入的数据了。
定义组件 FacyList
,并将item
传入:
1 | <ul> |
或者:
1 | <ul> |
注意:插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。
在父组件中引用子组件 FancyList
,可以使用传入的参数:
1 | <FancyList> |
无渲染组件
一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件。
大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销。
依赖注入
深层的组件需要顶层的数据,如果通过层层组件逐级传递 props,会很麻烦,中间层的组件可能根本不关系这些 props。
这个问题被称为 “prop 逐级透传”。

provide
(提供) 和 inject
(注入) 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

provide(提供)
要为组件后代提供数据,需要使用到 provide()
函数:
1 | <script setup> |
注入名 可以是字符串或是 Symbol
。
后代组件会用注入名来查找期望注入的值。
可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值。
值 可以是任意类型,包括响应式的状态,比如一个 ref:
1 | import { ref, provide } from "vue" |
提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
应用层 provide
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
1 | import { createApp } from "vue" |
在应用级别提供的数据在该应用内的所有组件中都可以注入。这在你编写插件时会特别有用,因为插件一般都不会使用组件形式来提供值。
inject(注入)
要注入上层组件提供的数据,需使用 inject()
函数:
1 | <script setup> |
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
注入默认值
1 | // 如果没有祖先组件提供 "message" |
或者:
1 | const value = inject('key', () => new ExpensiveClass()) |
和响应式数据配合使用
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:
1 | <!-- 在供给方组件内 --> |
1 | <!-- 在注入方组件 --> |
最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly()
来包装提供的值。
1 | <script setup> |
使用 symbol 作为注入名
如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
1 | // keys.js |
1 | // 在供给方组件中 |
1 | // 注入方组件 |
异步组件
基本用法
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
1 | import { defineAsyncComponent } from "vue" |
如你所见,defineAsyncComponent
方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve
回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason)
表明加载失败。
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用。类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),因此我们也可以用它来导入 Vue 单文件组件:
1 | import { defineAsyncComponent } from "vue" |
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
加载与错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
1 | const AsyncComp = defineAsyncComponent({ |
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
搭配 Suspense 使用
异步组件可以搭配内置的 <Suspense>
组件一起使用,若想了解 <Suspense>
和异步组件之间交互,请参阅 Suspense
章节。
逻辑复用
组合式函数
复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
在 Vue 中,复用有状态逻辑使用“组合式函数”(Composables) 。
和组件一样,可以在组合式函数中使用所有的 组合式 API,并返回需要暴露的状态。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
异步状态示例示例
1 | // fetch.js |
1 | <script lang="ts" setup> |
推荐使用 TanStack Query 库。
约定和最佳实践
命名
组合式函数约定用驼峰命名法命名,并以“use”作为开头。
输入参数
返回值
你可能已经注意到了,我们一直在组合式函数中使用 ref()
而不是 reactive()
。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
1 | // x 和 y 是两个 ref |
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive()
包装一次,这样其中的 ref 会被自动解包,例如:
1 | const mouse = reactive(useMouse()) |
1 | Mouse position is at: {{ mouse.x }}, {{ mouse.y }} |
副作用
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
如果你的应用用到了服务端渲染 (SSR),请确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,例如:
onMounted()
。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。确保在
onUnmounted()
时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在onUnmounted()
中被移除。1
2
3
4
5
6
7
8
9// event.js
import { onMounted, onUnmounted } from "vue"
export function useEventListener(target, event, callback) {
// 如果你想的话,
// 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素
onMounted(() => target.addEventListener(event, callback))
onUnmounted(() => target.removeEventListener(event, callback)) // 清理
}
使用限制
组合式函数在 <script setup>
或 setup()
钩子中,应始终被同步地调用。在某些场景下,你也可以在像 onMounted()
这样的生命周期钩子中使用他们。
这个限制是为了让 Vue 能够确定当前正在被执行的到底是哪个组件实例,只有能确认当前组件实例,才能够:
- 将生命周期钩子注册到该组件实例上;
- 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
提示:
<script setup>
是唯一在调用await
之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
自定义指令
除了 Vue 内置的一系列指令 (比如 v-model
或 v-show
) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
我们已经介绍了两种在 Vue 中重用代码的方式:组件和组合式函数。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。下面是一个自定义指令的例子,当一个 input 元素被 Vue 插入到 DOM 中后,它会被自动聚焦:
1 | <script setup> |
假设你还未点击页面中的其他地方,那么上面这个 input 元素应该会被自动聚焦。该指令比 autofocus
attribute 更有用,因为它不仅仅可以在页面加载完成后生效,还可以在 Vue 动态插入元素后生效。
在 <script setup>
中,任何以 v
开头的驼峰式命名的变量都可以被用作一个自定义指令。在上面的例子中,vFocus
即可以在模板中以 v-focus
的形式使用。
将一个自定义指令全局注册到应用层级也是一种常见的做法:
1 | const app = createApp({}) |
提示:
只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用
v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
指令钩子
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
1 | const myDirective = { |
钩子参数
简化形式
对象字面量
在组件上使用
当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传 attributes 类似。
总的来说,不推荐在组件上使用自定义指令。
插件
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。下面是如何安装一个插件的示例:
1 | import { createApp } from "vue" |
一个插件可以是一个拥有 install()
方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use()
的额外选项作为参数:
1 | const myPlugin = { |
插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:
- 通过
app.component()
和app.directive()
注册一到多个全局组件或自定义指令。 - 通过
app.provide()
使一个资源 可被注入 进整个应用。 - 向
app.config.globalProperties
中添加一些全局实例属性或方法 - 一个可能上述三种都包含了的功能库 (例如 vue-router)。
编写一个插件
…
内置组件
Transition
在一个元素或组件进入和离开 DOM 时应用动画。
TransitionGroup
在一个 v-for
列表中的元素或组件被插入,移动,或移除时应用动画。
KeepAlive
想要组件能在被“切走”的时候保留它们的状态。
可以用 <KeepAlive>
内置组件将这些动态组件包装起来。
Teleport
它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
Suspense
<Suspense>
是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。