Vue3官网学习
开始
简介
什么是Vue?
Vue是一款用于构建用户界面的JavaScript
框架.它基于标准的HTML
,CSS
,和JavaScript
构建,并提供了一套声明式的,组件化的编程模型,帮助你高效地开发用户界面.无论是简单还是复杂的界面,Vue都可以胜任.
下面是一个最基本的示例:
1 | <div id="app"> |
上面的示例展示了Vue
的两个核心功能:
声明式渲染:Vue
基于HTML
拓展了一套模板语法,使得我们可以声明式地描述出最终输出的HTML
和JavaScript
状态之间的关系.
响应性: Vue
会自动跟踪JavaScript
的状态并在其发生变化时响应式地更新DOM
渐进式框架
Vue
是一个框架,也是一个生态.其功能覆盖了大部分前端开发常见的需求.但Web
世界是十分多样化的,不同的开发者在Web
上构建的东西可能在形式和规模上会有很大的不同.考虑到这一点,Vue
的设计非常注重灵活性和”可以被逐步集成”这个特点.根据你的需求场景,你可以用不同的方式使用Vue
:
- 无需构建步骤,渐进式增强静态的
HTML
- 在任何页面中作为
Web Components
嵌入 - 单页应用(SPA)
- 全栈/服务端渲染(SSR)
Jamstack
/静态站点生成(SSG)- 开发桌面端,移动端,
WebGL
,甚至是命令行终端中的界面
单文件组件
在大多数启用了构建工具的Vue
项目中,我们可以使用一种类似HTML
格式的文件来书写Vue
组件,它被称为单文件组件(也被称为*.vue
文件,Single-File Components,SFC).顾名思义,Vue
的单文件组件会将一个组件的逻辑(JavaScript
),模板(HTML
)和样式(CSS
)封装在同一个文件里.下面我们将用单文件组件的格式重写上面的计数器示例.
1 | <script setup> |
单文件组件是Vue
的标志性功能.如果你的用例需要进行构建,我们推荐用它来编写Vue
组件.
API风格
Vue
的组件可以按照两种不同的风格书写: 选项式API和组合式API
选项式API(Options API)
使用选项式API,我们可以用包含多个选项的对象来描述组件的逻辑,例如data
,methods
和mounted
.选项所定义的属性都会暴露在函数内部的this
上,它会指向当前组件实例.
1 | <script> |
组合式API(Composition API)
通过组合式API,我们可以使用导入的API函数来描述组件逻辑.在单文件组件中,组合是API通常会与<script setup>
搭配使用.这个setup
属性是一个标识,告诉Vue
需要在编译时进行一些处理,让我们可以更简洁地使用组合式API.比如<script setup>
中的导入和顶层变量/函数都能够在模板中直接使用.
下面是使用了组合式API与<script setup>
改造后和上面的模板完全一样的组件.
1 | <script setup> |
该选哪一个?
两种API风格都能够覆盖大部分的应用场景.它们只是同一个底层系统所提供的两套不同的接口.实际上,选项式API是在组合式API的基础上实现的!关于Vue的基础概念和知识在他们之间都是通用的.
选项式API以”组件实例”的概念为中心(即上述例子中的this
),对于有面向对象语言背景的用户来说,这通常与基于类的心智模型更为一致.同时,它将响应性相关的细节抽象出来,并强制按照选项来组织代码,从而对初学者而言更为友好.
组合是API的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题.这种形式更自由,也需要你对Vue
的响应式系统有更深的理解才能高效使用.相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大.
在组合式API章节中,捏可以了解更多关于这两种API风格的对比以及组合式API所带来的潜在收益.
如果你是使用 Vue 的新手,这里是我们的大致建议:
- 在学习的过程中,推荐采用更易于自己理解的风格。再强调一下,大部分的核心概念在这两种风格之间都是通用的。熟悉了一种风格以后,你也能够很快地理解另一种风格。
- 在生产项目中:
- 当你不需要使用构建工具,或者打算主要在低复杂度的场景中使用 Vue,例如渐进增强的应用场景,推荐采用选项式 API。
- 当你打算用 Vue 构建完整的单页应用,推荐采用组合式 API + 单文件组件。
在学习阶段,你不必只固守一种风格。在接下来的文档中我们会为你提供一系列两种风格的代码供你参考,你可以随时通过左上角的 API 风格偏好来做切换。
快速上手
创建一个Vue应用
前提条件
- 熟悉命令行
- 安装18.3或更高版本的
Node.js
通过CDN使用Vue
启用Import maps
我们可以使用导入映射表(Import Maps)来告诉浏览器如何定位到导入的vue
1 | <script type="importmap"> |
你也可以在映射表中添加其他的依赖——但请务必确保你使用的是该库的 ES 模块版本。
拆分模块
随着对这份指南的逐步深入,我们可能需要将代码分割成单独的 JavaScript 文件,以便更容易管理。例如:
1 | <!-- index.html --> |
1 | // my-component.js |
如果直接在浏览器中打开了上面的 index.html
,你会发现它抛出了一个错误,因为 ES 模块不能通过 file://
协议工作,也即是当你打开一个本地文件时浏览器使用的协议。
由于安全原因,ES 模块只能通过 http://
协议工作,也即是浏览器在打开网页时使用的协议。为了使 ES 模块在我们的本地机器上工作,我们需要使用本地的 HTTP 服务器,通过 http://
协议来提供 index.html
。
要启动一个本地的 HTTP 服务器,请先安装 Node.js,然后通过命令行在 HTML 文件所在文件夹下运行 npx serve
。你也可以使用其他任何可以基于正确的 MIME 类型服务静态文件的 HTTP 服务器。
可能你也注意到了,这里导入的组件模板是内联的 JavaScript 字符串。如果你正在使用 VS Code,你可以安装 es6-string-html 扩展,然后在字符串前加上一个前缀注释 /*html*/
以高亮语法。
基础
创建一个应用
应用实例
每个Vue应用都是通过createApp
函数创建一个新的应用实例:
1 | import { createApp } from 'vue' |
根组件
我们传入createApp
的对象实际上是一个组件,每个应用都需要一个”根组件”,其他组件将作为其子组件爱你.
如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件
1 | <script setup> |
虽然本指南中的许多示例只需要一个组件,但大多数真实的应用都是由一棵嵌套的、可重用的组件树组成的。例如,一个待办事项 (Todos) 应用的组件树可能是这样的:
1 | App (root component) |
我们会在指南的后续章节中讨论如何定义和组合多个组件。在那之前,我们得先关注一个组件内到底发生了什么。
挂载应用
应用实例必须在调用了.mount()
方法后才会渲染出来.该方法接受一个”容器”参数,可以是一个实际的DOM元素或是一个CSS选择器字符串.
1 | <div id="app"></div> |
1 | app.mount('##app') |
应用根组件的内容将会被渲染在容器元素里面,容器元素自己将不会被视为应用的一部分.
.mount()
方法应该始终在整个应用配置和资源注册完成后被调用.同时请注意,不同于其他资源注册方法,它的返回值是根组件实例,而非应用实例.
DOM中的根组件模板
根组件的模板通常是组件本身的一部分,但也可以直接通过在挂载容器内编写模板来单独提供.
1 | <div id="app"> |
1 | import { createApp } from 'vue' |
当根组件没有设置template
选项时,Vue将自动使用容器的innerHTML
作为模板.
DOM 内模板通常用于无构建步骤的 Vue 应用程序。它们也可以与服务器端框架一起使用,其中根模板可能是由服务器动态生成的。
应用配置
应用实例会暴露一个.config
对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误.
1 | app.config.errorHandler = (err) => { |
应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:
1 | app.component('TodoDeleteButton', TodoDeleteButton) |
这使得TodoDeleteButton
在应用的任何地方都是可用的.我们会在指南的后续章节中讨论关于组件和其他资源的注册。你也可以在 API 参考中浏览应用实例 API 的完整列表。
确保在挂载应用实例之前完成所有应用配置!
多个应用实例
应用实例并不只限于一个。createApp
API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
1 | const app1 = createApp({ |
如果你正在使用 Vue 来增强服务端渲染 HTML,并且只想要 Vue 去控制一个大型页面中特殊的一小部分,应避免将一个单独的 Vue 应用实例挂载到整个页面上,而是应该创建多个小的应用实例,将它们分别挂载到所需的元素上去。
模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。
文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
1 | <template> |
双大括号标签会被替换为相应组件实例中 msg
属性的值。同时每次 msg
属性更改时它也会同步更新。
原始HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html
指令:
1 | <template> |
这里我们遇到了一个新的概念。这里看到的 v-html
attribute 被称为一个指令。指令由 v-
作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML
与 rawHtml
属性保持同步。
span
的内容将会被替换为 rawHtml
属性的值,插值为纯 HTML——数据绑定将会被忽略。注意,你不能使用 v-html
来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。
安全警告
在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用
v-html
,并且永远不要使用用户提供的 HTML 内容。
Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind
指令:
1 | <tempalte> |
v-bind
指令指示 Vue 将元素的 id
attribute 与组件的 dynamicId
属性保持一致。如果绑定的值是 null
或者 undefined
,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind
非常常用,我们提供了特定的简写语法:
1 | <tempalte> |
开头为 :
的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中。简写语法是可选的,但相信在你了解了它更多的用处后,你应该会更喜欢它。
接下来的指引中,我们都将在示例中使用简写语法,因为这是在实际开发中更常见的用法。
同名简写
- 仅支持3.4版本及以上
如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:
1 | <template> |
这与在 JavaScript 中声明对象时使用的属性简写语法类似。请注意,这是一个只在 Vue 3.4 及以上版本中可用的特性。
布尔型Atrribute
布尔型attribute
依据true/false
来决定attribute
是否应该存在于该元素上.disabled
就是最常见的例子之一.
v-bind
在这种场景下的行为略有不同.
1 | <template> |
当 isButtonDisabled
为真值或一个空字符串 (即 <button disabled="">
) 时,元素会包含这个 disabled
attribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute
的 JavaScript
对象:
1 | const objectOfAttrs = { |
通过不带参数的 v-bind
,你可以将它们绑定到单个元素上:
1 | <tempalte> |
使用JavaScript
表达式
至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
1 | <template> |
这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。
在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以
v-
开头的特殊 attribute) attribute 的值中
仅支持表达式
每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return
后面。
因此,下面的例子都是无效的:
1 | <!-- 这是一个语句,而非表达式 --> |
调用函数
可以在绑定的表达式中使用一个组件暴露的方法:
1 | <template> |
绑定在表达式中的方法在组件每次更新时都会被重新调用,因此不应该产生任何副作用,比如改变数据或触发异步操作。
受限的全局访问
模板中的表达式将被沙盒化,仅能够访问到有限的全局对象列表。该列表中会暴露常用的内置全局对象,比如 Math
和 Date
。
没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window
上的属性。然而,你也可以自行在 app.config.globalProperties
上显式地添加它们,供所有的 Vue 表达式使用。
指令Directives
指令是带有 v-
前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind
和 v-html
。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for
、v-on
和 v-slot
)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if
为例:
1 | <template> |
这里,v-if
指令会基于表达式 seen
的值的真假来移除/插入该 <p>
元素。
参数Arguments
某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind
指令来响应式地更新一个 HTML attribute:
1 | <template> |
这里 href
就是一个参数,它告诉 v-bind
指令将表达式 url
的值绑定到元素的 href
attribute 上。在简写中,参数前的一切 (例如 v-bind:
) 都会被缩略为一个 :
字符。
另一个例子是 v-on
指令,它将监听 DOM 事件:
1 | <template> |
这里的参数是要监听的事件名称:click
。v-on
有一个相应的缩写,即 @
字符。我们之后也会讨论关于事件处理的更多细节。
动态参数
同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
1 | <template> |
这里的 attributeName
会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName
,其值为 "href"
,那么这个绑定就等价于 v-bind:href
。
相似地,你还可以将一个函数绑定到动态的事件名称上:
1 | <template> |
在此示例中,当 eventName
的值是 "focus"
时,v-on:[eventName]
就等价于 v-on:focus
。
## 动态参数的限制
动态参数中表达式的值应当是一个字符串,或者是 null
。特殊值 null
意为显式移除该绑定。其他非字符串的值会触发警告。
## 动态参数的语法限制
动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:
1 | <template> |
如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。
当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:
1 | <a :[someAttr]="value"> ... </a> |
上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]
。如果你的组件拥有 “someAttr”
属性而非 “someattr”
,这段代码将不会工作。单文件组件内的模板不受此限制
修饰符 Modifiers
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent
修饰符会告知 v-on
指令对触发的事件调用 event.preventDefault()
:
之后在讲到 v-on
和 v-model
的功能时,你将会看到其他修饰符的例子。
最后,在这里你可以直观地看到完整的指令语法:
响应式基础
声明响应式状态
ref()
在组合式API中,推荐使用ref()
函数来声明响应式状态
1 | import { ref } from 'vue' |
ref()
接收参数,并将其包裹在一个带有 .value
属性的 ref 对象中返回:
1 | const count = ref(0) |
参考: 为 refs 标注类型
要在组件模板中访问ref,请从组件的setup()
函数中声明并返回它们:
1 | import { ref } from 'vue' |
1 | <template> |
注意,在模板中使用 ref 时,我们不需要附加 .value
。为了方便起见,当在模板中使用时,ref 会自动解包 (有一些注意事项)。
你也可以直接在事件监听器中改变一个 ref:
1 | <template> |
对于更复杂的逻辑,我们可以在同一作用域内声明更改ref
的函数,并将它们作为方法与状态一起公开
1 | import { ref } from 'vue' |
然后,暴露的方法可以被用作事件监听器:
1 | <template> |
这里是 Codepen 上的例子,没有使用任何构建工具。
<script setup>
在setup()
函数中手动暴露大量的状态和方法非常繁琐.幸运的是,我们可以通过使用单文件组件 (SFC) 来避免这种情况。我们可以使用 <script setup>
来大幅度地简化代码:
1 | <script setup> |
<script setup>
中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用。你可以理解为模板是在同一作用域内声明的一个 JavaScript
函数——它自然可以访问与它一起声明的所有内容。
在指南的后续章节中,我们基本上都会在组合式 API 示例中使用单文件组件 +
<script setup>
的语法,因为大多数 Vue 开发者都会这样使用。如果你没有使用单文件组件,你仍然可以在
setup()
选项中使用组合式 API。
为什么使用ref
你可能会好奇:为什么我们需要使用带有 .value
的 ref,而不是普通的变量?为了解释这一点,我们需要简单地讨论一下 Vue 的响应式系统是如何工作的。
当你在模板中使用了一个 ref,然后改变了这个 ref 的值时,Vue 会自动检测到这个变化,并且相应地更新 DOM。这是通过一个基于依赖追踪的响应式系统实现的。当一个组件首次渲染时,Vue 会追踪在渲染过程中使用的每一个 ref。然后,当一个 ref 被修改时,它会触发追踪它的组件的一次重新渲染。
在标准的 JavaScript
中,检测普通变量的访问或修改是行不通的。然而,我们可以通过 getter 和 setter 方法来拦截对象属性的 get 和 set 操作。
该 .value
属性给予了 Vue 一个机会来检测 ref 何时被访问或修改。在其内部,Vue 在它的 getter 中执行追踪,在它的 setter 中执行触发。从概念上讲,你可以将 ref 看作是一个像这样的对象:
1 | // 伪代码,不是真正的实现 |
另一个 ref 的好处是,与普通变量不同,你可以将 ref 传递给函数,同时保留对最新值和响应式连接的访问。当将复杂的逻辑重构为可重用的代码时,这将非常有用。
该响应性系统在深入响应式原理章节中有更详细的讨论。
深层响应性
Ref可以持有任何类型的值,包括深层嵌套的对象,数组或者JavaScript
内置的数据结构,比如Map
Ref会使它的值具有深层的响应性.这意味着即使改变嵌套对象或数组时,变化也会被检测到:
1 | import { ref } from 'vue' |
非原始值将通过 reactive()
转换为响应式代理,该函数将在后面讨论。
也可以通过 shallow ref 来放弃深层响应性。对于浅层 ref,只有 .value
的访问会被追踪。浅层 ref 可以用于避免对大型数据的响应性开销来优化性能、或者有外部库管理其内部状态的情况。
阅读更多:
DOM更新时机
当你修改了响应式状态时,DOM会被自动更新,但需要注意的是,DOM更新不是同步的.
Vue会在”next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次.
要等待DOM更新完成后在执行额外的代码,可以使用nextTick() 全局 API:
1 | import { nextTick } from 'vue' |
reactive()
还有另一种声明响应式状态的方式,即使用reactive()
API.与将内部值包装在特殊对象中的ref不同,reactive()
将使对象本身具有响应性.
1 | import { reactive } from 'vue' |
在模板中使用
1 | <template> |
响应式对象是 JavaScript 代理,其行为就和普通对象一样。不同的是,Vue 能够拦截对响应式对象所有属性的访问和修改,以便进行依赖追踪和触发更新。
reactive()
将深层地转换对象:当访问嵌套对象时,它们也会被 reactive()
包装。当 ref 的值是一个对象时,ref()
也会在内部调用它。与浅层 ref 类似,这里也有一个 shallowReactive()
API 可以选择退出深层响应性。
Reactive Proxy vs. Original
值得注意的是,reactive()
返回的是一个原始对象的 Proxy,它和原始对象是不相等的:
1 | const row = {} |
只有代理对象是响应式的,更改原始对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本。
为保证访问代理的一致性,对同一个原始对象调用 reactive()
会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive()
会返回其本身:
1 | // 在同一个对象上调用 reactive() 会返回相同的代理 |
这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:
1 | const proxy = reactive({}) |
reactive()
的局限性
reactive()
API有一些局限性:
- 有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。 - 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失:
1 | let state = reactive({ count: 0 }) |
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:
1 | const state = reactive({ count: 0 }) |
由于这些限制,我们建议使用 ref()
作为声明响应式状态的主要 API。
额外的ref解包细节[这里暂时不是很理解]
作为reactive对象的属性
一个 ref 会在作为响应式对象的属性被访问或修改时自动解包。换句话说,它的行为就像一个普通的属性:
1 | const count = ref(0) |
如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref:
1 | const otherCount = ref(2) |
只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包。
数据和集合的注意事项
与reactive对象不同的是,当ref作为响应式数组或原生集合类型(如Map
)中的元素被访问时,它不会被解包.
1 | const books = reactive([ref('Vue3 Guide')]) |
在模板中解包的注意事项
在模板渲染上下文中,只有顶级的 ref 属性才会被解包。
在下面的例子中,count
和 object
是顶级属性,但 object.id
不是:
1 | const count = ref(0) |
因此,这个表达式按预期工作:
1 | {{ count + 1 }} |
…但这个不会:
1 | {{ object.id + 1 }} |
渲染的结果将是 [object Object]1
,因为在计算表达式时 object.id
没有被解包,仍然是一个 ref 对象。为了解决这个问题,我们可以将 id
解构为一个顶级属性:
1 | const { id } = object |
1 | {{ id + 1 }} |
现在渲染的结果将是 2
。
另一个需要注意的点是,如果 ref 是文本插值的最终计算值 (即 {{ }}
标签),那么它将被解包,因此以下内容将渲染为 1
:
1 | {{ object.id }} |
该特性仅仅是文本插值的一个便利特性,等价于 {{ object.id.value }}
。
计算属性
基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
1 | const author = reactive({ |
我们想根据 author
是否已有一些书籍来展示不同的信息:
1 | <template> |
这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
1 | <script setup> |
我们在这里定义了一个计算属性 publishedBooksMessage
。computed()
方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value
访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value
。
Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage
依赖于 author.books
,所以当 author.books
改变时,任何依赖于 publishedBooksMessage
的绑定都会同时更新。
也可参考:为计算属性标注类型
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
1 | <template> |
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
会随之更新。
获取上一个值
- 仅3.4+支持
如果需要,可以通过访问计算属性的 getter 的第一个参数来获取计算属性返回的上一个值:
1 | <script setup> |
如果你正在使用可写的计算属性的话:
1 | <script setup> |
最佳实践
Getter不应有副作用
计算属性的 getter 应只做计算而没有任何其他的副作用,这一点非常重要,请务必牢记。举例来说,不要改变其他状态、在 getter 中做异步请求或者更改 DOM!一个计算属性的声明中描述的是如何根据其他值派生一个值。因此 getter 的职责应该仅为计算和返回该值。在之后的指引中我们会讨论如何使用侦听器根据其他响应式状态的变更来创建副作用。
避免直接修改计算属性的值
从计算属性返回的值是派生状态。可以把它看作是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。
类与样式绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class
列表和内联样式。因为 class
和 style
都是 attribute,我们可以和其他 attribute 一样使用 v-bind
将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class
和 style
的 v-bind
用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定HTML class
绑定对象
我们可以给 :class
(v-bind:class
的缩写) 传递一个对象来动态切换 class:
1 | <template> |
上面的语法表示 active
是否存在取决于数据属性 isActive
的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class
指令也可以和一般的 class
attribute 共存。举例来说,下面这样的状态:
1 | const isActive = ref(true) |
配合以下模板:
1 | <template> |
渲染的结果会是
1 | <div class="static active"></div> |
当 isActive
或者 hasError
改变时,class 列表会随之更新。举例来说,如果 hasError
变为 true
,class 列表也会变成 "static active text-danger"
。
绑定的对象并不一定需要写成内联字面量的形式,也可以直接绑定一个对象:
1 | const classObject = reactive({ |
1 | <div :class="classObject"></div> |
这将渲染:
1 | <div class="active"></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> |
errorClass
会一直存在,但 activeClass
只会在 isActive
为真时才存在。
然而,这可能在有多个依赖条件的 class 时会有些冗长。因此也可以在数组中嵌套对象:
1 | <div :class="[{ [activeClass]: isActive }, errorClass]"></div> |
在组件上使用
本节假设你已经有 Vue 组件的知识基础。如果没有,你也可以暂时跳过,以后再阅读。
对于只有一个根元素的组件,当你使用了 class
attribute 时,这些 class 会被添加到根元素上并与该元素上已有的 class 合并。
举例来说,如果你声明了一个组件名叫 MyComponent
,模板如下:
1 | <!-- 子组件模板 --> |
在使用时添加一些 class:
1 | <!-- 在使用组件时 --> |
渲染出的 HTML 为:
1 | <p class="foo bar baz boo">Hi!</p> |
Class 的绑定也是同样的:
1 | <MyComponent :class="{ active: isActive }" /> |
当 isActive
为真时,被渲染的 HTML 会是:
1 | <p class="foo bar active">Hi!</p> |
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs
属性来指定接收的元素
1 | <!-- MyComponent 模板使用 $attrs 时 --> |
1 | <MyComponent class="baz" /> |
这将被渲染为:
1 | <p class="baz">Hi!</p> |
你可以在透传 Attribute 一章中了解更多组件的 attribute 继承的细节。
绑定内联样式
绑定对象
:style
支持绑定JavaScript
对象值,对应的是HTML 元素的 style
属性:
1 | const activeColor = ref('red') |
1 | <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> |
尽管推荐使用 camelCase,但 :style
也支持 kebab-cased 形式的 CSS 属性 key (对应其 CSS 中的实际名称),例如:
1 | <div :style="{ 'font-size': fontSize + 'px' }"></div> |
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
1 | const styleObject = reactive({ |
1 | <div :style="styleObject"></div> |
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
绑定数组
我们还可以给 :style
绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
1 | <div :style="[baseStyles, overridingStyles]"></div> |
自动前缀
当你在 :style
中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
1 | <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div> |
数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
条件渲染
列表渲染
通过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 |
不自动将 item
注入组件的原因是,这会使组件与 v-for
的工作方式紧密耦合。明确其数据的来源可以使组件在其他情况下重用。
这里是一个简单的 Todo List 的例子,展示了如何通过 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="handler"
或 @click="handler"
。
事件处理器 (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
访问到该 DOM 元素。
你也可以看看为事件处理器标注类型这一章了解更多。
方法与内联事件判断
模板编译器会通过检查 v-on
的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo
、foo.bar
和 foo['bar']
会被视为方法事件处理器,而 foo()
和 count++
会被视为内联事件处理器。
在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
1 | function say(message) { |
1 | <button @click="say('hello')">Say hello</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
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
但请注意,.left
,.right
和 .middle
这些修饰符名称是基于常见的右手用鼠标布局设定的,但实际上它们分别指代设备事件触发器的“主”、”次“,“辅助”,而非实际的物理按键。因此,对于左手用鼠标布局而言,“主”按键在物理上可能是右边的按键,但却会触发 .left
修饰符对应的处理程序。又或者,触控板可能通过单指点击触发 .left
处理程序,通过双指点击触发 .right
处理程序,通过三指点击触发 .middle
处理程序。同样,产生“鼠标”事件的其他设备和事件源,也可能具有与“左”,“右”完全无关的触发模式。
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
1 | <input |
v-model
指令帮我们简化了这一步骤:
1 | <input v-model="text"> |
另外,v-model
还可以用于各种不同类型的输入,<textarea>
、<select>
元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>
和<textarea>
元素会绑定value
property 并侦听input
事件; <input type="checkbox">
和<input type="radio">
会绑定checked
property 并侦听change
事件;<select>
会绑定value
property 并侦听change
事件。
v-model
会忽略任何表单元素上初始的value
、checked
或selected
attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API来声明该初始值。
基本用法
文本
1 | <p>Message is: {{ message }}</p> |
对于需要使用 IME 的语言 (中文,日文和韩文等),你会发现
v-model
不会在 IME 输入还在拼字阶段时触发更新。如果你的确想在拼字阶段也触发更新,请直接使用自己的input
事件监听器和value
绑定而不要使用v-model
。
多行文本
1 | <span>Multiline message is:</span> |
注意在 <textarea>
中是不支持插值表达式的。请使用 v-model
来替代:
1 | <!-- 错误 --> |
复选框
单一的复选框,绑定布尔类型值:
1 | <input type="checkbox" id="checkbox" v-model="checked" /> |
我们也可以将多个复选框绑定到同一个数组或集合的值:
1 | const checkedNames = ref([]) |
1 | <div>Checked names: {{ checkedNames }}</div> |
单选按钮
1 | <div>Picked: {{ picked }}</div> |
选择器
单个选择器的示例如下:
1 | <div>Selected: {{ selected }}</div> |
如果
v-model
表达式的初始值不匹配任何一个选择项,<select>
元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
多选 (值绑定到一个数组):
1 | <div>Selected: {{ selected }}</div> |
选择器的选项可以使用v-for
动态渲染.
1 | const selected = ref('A') |
1 | <select v-model="selected"> |
值绑定
对于单选按钮,复选框和选择器选项,v-model
绑定的值通常是静态的字符串 (或者对复选框是布尔值):
1 | <!-- `picked` 在被选择时是字符串 "a" --> |
但有时我们可能希望将该值绑定到当前组件实例上的动态数据。这可以通过使用 v-bind
来实现。此外,使用 v-bind
还使我们可以将选项值绑定为非字符串的数据类型。
复选框
1 | <input |
true-value
和 false-value
是 Vue 特有的 attributes,仅支持和 v-model
配套使用。这里 toggle
属性的值会在选中时被设为 'yes'
,取消选择时设为 'no'
。你同样可以通过 v-bind
将其绑定为其他动态值:
1 | <input |
true-value
和false-value
attributes 不会影响value
attribute,因为浏览器在表单提交时,并不会包含未选择的复选框。为了保证这两个值 (例如:“yes”和“no”) 的其中之一被表单提交,请使用单选按钮作为替代。
单选按钮
1 | <input type="radio" v-model="pick" :value="first" /> |
pick
会在第一个按钮选中时被设为 first
,在第二个按钮选中时被设为 second
。
选择器选项
1 | <select v-model="selected"> |
v-model
同样也支持非字符串类型的值绑定!在上面这个例子中,当某个选项被选中,selected
会被设为该对象字面量值 { number: 123 }
。
修饰符
.lazy
默认情况下,v-model
会在每次 input
事件后更新数据 (IME 拼字阶段的状态例外)。你可以添加 lazy
修饰符来改为在每次 change
事件后更新数据:
1 | <!-- 在 "change" 事件后同步更新而不是 "input" --> |
.number
如果你想让用户输入自动转换为数字,你可以在 v-model
后添加 .number
修饰符来管理输入:
1 | <input v-model.number="age" /> |
如果该值无法被 parseFloat()
处理,那么将返回原始值。特别是当输入为空时 (例如用户清空输入字段之后),会返回一个空字符串。这种行为与 DOM 属性 valueAsNumber 有所不同。
number
修饰符会在输入框有 type="number"
时自动启用。
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model
后添加 .trim
修饰符:
1 | <input v-model.trim="msg" /> |
生命周期
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
注册周期钩子
举例来说,onMounted
钩子可以用来在组件完成初始渲染并创建DOM节点后运行代码.
1 | <script setup> |
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 onMounted
、onUpdated
和 onUnmounted
。所有生命周期钩子的完整参考及其用法请参考 API 索引。
当调用 onMounted
时,Vue 会自动将回调函数注册到当前正被初始化的组件实例上。这意味着这些钩子应当在组件初始化时被同步注册。例如,请不要这样做:
1 | setTimeout(() => { |
注意这并不意味着对 onMounted
的调用必须放在 setup()
或 <script setup>
内的词法上下文中。onMounted()
也可以在一个外部函数中调用,只要调用栈是同步的,且最终起源自 setup()
就可以。
生命周期示意图
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
有关所有生命周期钩子及其各自用例的详细信息,请参考生命周期钩子 API 索引。
侦听器
基本示例
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 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( |
在 Vue 3.5+ 中,deep
选项还可以是一个数字,表示最大遍历深度——即 Vue 应该遍历对象嵌套属性的级数。
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的监听器
watch
默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以通过传入 immediate: true
选项来强制侦听器的回调立即执行:
1 | watch( |
一次性监听器
- 仅支持3.4及以上版本
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true
选项。
1 | watch( |
watchEffect()
侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId
的引用发生变化时使用侦听器来加载一个远程资源:
1 | const todoId = ref(1) |
特别是注意侦听器是如何两次使用 todoId
的,一次是作为源,另一次是在回调中。
我们可以用 watchEffect
函数 来简化上面的代码。watchEffect()
允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:
1 | watchEffect(async () => { |
这个例子中,回调会立即执行,不需要指定 immediate: true
。在执行期间,它会自动追踪 todoId.value
作为依赖(和计算属性类似)。每当 todoId.value
变化时,回调会再次执行。有了 watchEffect()
,我们不再需要明确传递 todoId
作为源值。
你可以参考一下这个例子的 watchEffect
和响应式的数据请求的操作。
对于这种只有一个依赖项的例子来说,watchEffect()
的好处相对较小。但是对于有多个依赖项的侦听器来说,使用 watchEffect()
可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect()
可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个await
正常工作前访问到的属性才会被追踪。
watch
vs. watchEffect
watch
和 watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:
watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
副作用清理
有时我们可能会在侦听器中执行副作用,例如异步请求:
1 | watch(id, (newId) => { |
但是如果在请求完成之前 id
发生了变化怎么办?当上一个请求完成时,它仍会使用已经过时的 ID 值触发回调。理想情况下,我们希望能够在 id
变为新值时取消过时的请求。
我们可以使用 onWatcherCleanup()
API 来注册一个清理函数,当侦听器失效并准备重新运行时会被调用:
1 | import { watch, onWatcherCleanup } from 'vue' |
请注意,onWatcherCleanup
仅在 Vue 3.5+ 中支持,并且必须在 watchEffect
效果函数或 watch
回调函数的同步执行期间调用:你不能在异步函数的 await
语句之后调用它。
作为替代,onCleanup
函数还作为第三个参数传递给侦听器回调,以及 watchEffect
作用函数的第一个参数:
1 | watch(id, (newId, oldId, onCleanup) => { |
这在 3.5 之前的版本有效。此外,通过函数参数传递的 onCleanup
与侦听器实例相绑定,因此不受 onWatcherCleanup
的同步限制。
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
Post Watchers
如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post'
选项:
1 | watch(source, callback, { |
后置刷新的 watchEffect()
有个更方便的别名 watchPostEffect()
:
1 | import { watchPostEffect } from 'vue' |
同步侦听器
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:
1 | watch(source, callback, { |
同步触发的 watchEffect()
有个更方便的别名 watchSyncEffect()
:
1 | import { watchSyncEffect } from 'vue' |
同步侦听器不会进行批处理,每当检测到响应式数据发生变化时就会触发。可以使用它来监视简单的布尔值,但应避免在可能多次同步修改的数据源 (如数组) 上使用。
停止侦听器
在 setup()
或 <script setup>
中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器。
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子
1 | <script setup> |
要手动停止一个侦听器,请调用 watch
或 watchEffect
返回的函数:
1 | const unwatch = watchEffect(() => {}) |
注意,需要异步创建侦听器的情况很少,请尽可能选择同步创建。如果需要等待一些异步数据,你可以使用条件式的侦听逻辑:
1 | // 需要异步请求得到的数据 |
模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref
attribute:
ref
是一个特殊的 attribute,和 v-for
章节中提到的 key
类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
访问模板引用
要在组合式 API 中获取引用,我们可以使用辅助函数 useTemplateRef()
:
1 | <script setup> |
在使用 TypeScript 时,Vue 的 IDE 支持和 vue-tsc
将根据匹配的 ref
attribute 所用的元素或组件自动推断 input.value
的类型。
3.5之前的用法
在 3.5 之前的版本尚未引入
useTemplateRef()
,我们需要声明一个与模板里 ref attribute 匹配的引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 <script setup>
import { ref, onMounted } from 'vue'
// 声明一个 ref 来存放该元素的引用
// 必须和模板里的 ref 同名
const input = ref(null)
onMounted(() => {
input.value.focus()
})
</script>
<template>
<input ref="input" />
</template>如果不使用
<script setup>
,需确保从setup()
返回 ref:
1
2
3
4
5
6
7
8
9 export default {
setup() {
const input = ref(null)
// ...
return {
input
}
}
}
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 input
,在初次渲染时会是 null
。这是因为在初次渲染前这个元素还不存在呢!
如果你需要侦听一个模板引用 ref 的变化,确保考虑到其值为 null
的情况:
1 | watchEffect(() => { |
也可参考:为模板引用标注类型
v-for中的模板引用
- 3.5及以上
当在 v-for
中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:
1 | <script setup> |
3.5之前的用法
在 3.5 版本以前,
useTemplateRef()
尚未引入,需要声明一个与模板引用 attribute 同名的 ref。该 ref 的值需要是一个数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 <script setup>
import { ref, onMounted } from 'vue'
const list = ref([
/* ... */
])
const itemRefs = ref([])
onMounted(() => console.log(itemRefs.value))
</script>
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
应该注意的是,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 都会自动解包,和一般的实例一样)。
TypeScript 用户请参考:为组件的模板引用标注类型
组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成一个层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。Vue 同样也能很好地配合原生 Web Component。如果你想知道 Vue 组件与原生 Web Components 之间的关系,可以阅读此章节。
定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue
文件中,这被叫做单文件组件 (简称 SFC):
1 | <script setup> |
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
1 | import { ref } from 'vue' |
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template>
元素),Vue 将会使用其内容作为模板来源。
上面的例子中定义了一个组件,并在一个 .js
文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。
使用组件
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue
的文件中,这个组件将会以默认导出的形式被暴露给外部。
1 | <script setup> |
通过 <script setup>
,导入的组件都在模板中直接可用。
当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。
组件可以被重用任意多次:
1 | <h1>Here is a child component!</h1> |
你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count
。这是因为每当你使用一个组件,就创建了一个新的实例。
在单文件组件中,推荐为子组件使用 PascalCase
的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 />
来关闭一个标签。
如果你是直接在 DOM 中书写模板 (例如原生 <template>
元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case
形式并显式地关闭这些组件的标签。
1 | <!-- 如果是在 DOM 中书写该模板 --> |
请看 DOM 内模板解析注意事项了解更多细节。
传递props
如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。
Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps
宏:
1 | <!-- BlogPost.vue --> |
defineProps
是一个仅 <script setup>
中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps
会返回一个对象,其中包含了可以传递给组件的所有 props:
1 | const props = defineProps(['title']) |
TypeScript 用户请参考:为组件 props 标注类型
如果你没有使用 <script setup>
,props 必须以 props
选项的方式声明,props 对象会作为 setup()
函数的第一个参数被传入:
1 | export default { |
一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。
当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:
1 | <BlogPost title="My journey with Vue" /> |
在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:
1 | const posts = ref([ |
这种情况下,我们可以使用 v-for
来渲染它们:
1 | <BlogPost |
留意我们是如何使用 v-bind
语法 (:title="post.title"
) 来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。
以上就是目前你需要了解的关于 props 的全部了。如果你看完本章节后还想知道更多细节,我们推荐你深入阅读关于 props 的完整指引。
监听事件
让我们继续关注我们的 <BlogPost>
组件。我们会发现有时候它需要与父组件进行交互。例如,要在此处实现无障碍访问的需求,将博客文章的文字能够放大,而页面的其余部分仍使用默认字号。
在父组件中,我们可以添加一个 postFontSize
ref 来实现这个效果:
1 | const posts = ref([ |
在模板中用它来控制所有博客文章的字体大小:
1 | <div :style="{ fontSize: postFontSize + 'em' }"> |
然后,给 <BlogPost>
组件添加一个按钮:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
这个按钮目前还没有做任何事情,我们想要点击这个按钮来告诉父组件它应该放大所有博客文章的文字。要解决这个问题,组件实例提供了一个自定义事件系统。父组件可以通过 v-on
或 @
来选择性地监听子组件上抛的事件,就像监听原生 DOM 事件那样:
1 | <BlogPost |
子组件可以通过调用内置的 $emit
方法,通过传入事件名称来抛出一个事件:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
我们可以通过 defineEmits
宏来声明需要抛出的事件:
1 | <!-- BlogPost.vue --> |
这声明了一个组件可能触发的所有事件,还可以对事件的参数进行验证。同时,这还可以让 Vue 避免将它们作为原生事件监听器隐式地应用于子组件的根元素。
和 defineProps
类似,defineEmits
仅可用于 <script setup>
之中,并且不需要导入,它返回一个等同于 $emit
方法的 emit
函数。它可以被用于在组件的 <script setup>
中抛出事件,因为此处无法直接访问 $emit
:
1 | <script setup> |
TypeScript 用户请参考:为组件 emits 标注类型
如果你没有在使用 <script setup>
,你可以通过 emits
选项定义组件会抛出的事件。你可以从 setup()
函数的第二个参数,即 setup 上下文对象上访问到 emit
函数:
1 | export default { |
以上就是目前你需要了解的关于组件自定义事件的所有知识了。如果你看完本章节后还想知道更多细节,请深入阅读组件事件章节。
通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
1 | <AlertBox> |
我们期望能渲染成这样:
这可以通过 Vue 的自定义 <slot>
元素来实现:
1 | <!-- AlertBox.vue --> |
如上所示,我们使用 <slot>
作为一个占位符,父组件传递进来的内容就会渲染在这里。
以上就是目前你需要了解的关于插槽的所有知识了。如果你看完本章节后还想知道更多细节,请深入阅读组件插槽章节。
动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
上面的例子是通过 Vue 的 <component>
元素和特殊的 is
attribute 实现的:
1 | <!-- currentTab 改变时组件也改变 --> |
在上面的例子中,被传给 :is
的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用 is
attribute 来创建一般的 HTML 元素。
当使用 <component :is="...">
来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 `` 组件强制被切换掉的组件仍然保持“存活”的状态。
DOM内模板解析的注意事项
如果你想在 DOM 中直接书写 Vue 模板,Vue 则必须从 DOM 中获取模板字符串。由于浏览器的原生 HTML 解析行为限制,有一些需要注意的事项。
请注意下面讨论只适用于直接在 DOM 中编写模板的情况。如果你使用来自以下来源的字符串模板,就不需要顾虑这些限制了:
- 单文件组件
- 内联模板字符串 (例如
template: '...'
)<script type="text/x-template">
深入组件
注册
一个Vue组件在使用前需要先被注册,这样Vue才能在渲染模板时找到其对应的实现.组件注册有两种方式:全局注册和局部注册.
全局注册
我们可以使用Vue 应用实例的 .component()
方法,让组件在当前 Vue 应用中全局可用。
1 | import { createApp } from 'vue' |
如果使用单文件组件,你可以注册被导入的 .vue
文件:
1 | import MyComponent from './App.vue' |
.component()
方法可以被链式调用:
1 | app |
全局注册的组件可以在此应用的任意组件的模板中使用:
1 | <!-- 这在当前应用的任意组件中都可用 --> |
所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用。
局部注册
全局注册虽然方便,但也有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除(也叫”tree-shaking”),如果你全局注册了一个组件,即使它没有被实际使用,它仍然会出现在打包后的JS文件中.
- 全局注册在大型项目中使项目的依赖关系变得不那么明确.在父组件中使用子组件时,不太容易定位子组件的实现.和使用过多的全局变量一样,这可能会影响应用长期的可维护性.
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在父组件中使用.它的优点是使组件之间的依赖关系更加明确,并且对tree-shaking
更加友好.
在使用 <script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
1 | <script setup> |
如果没有使用 <script setup>
,则需要使用 components
选项来显式注册:
1 | import ComponentA from './ComponentA.js' |
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:
1 | export default { |
请注意:局部注册的组件在后代组件中*不*可用。在这个例子中,ComponentA
注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用。
组件名格式
在整个指引中,我们都使用PascalCase
作为组件名的注册格式,这是因为:
PascalCase
是合法的JavaScript
标识符.这使得在JavaScript
中导入和注册都很容易,同时IDE
也能提供较好的自动补全.<PascalCase />
在模板中更明显地表明了这是一个Vue
组件,而不是原生的HTML
元素.同时也能够将Vue组件和自定义元素(web components
)区分开来.
在单文件组件和内联字符串模板中,我们都推荐这样做.但是,PascalCase
的标签名在DOM模板中是不可用的,详情参见DOM 内模板解析注意事项。
为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent
为名注册的组件,在模板 (或由 Vue 渲染的 HTML 元素) 中可以通过 <MyComponent>
或 <my-component>
引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。
Props
Props声明
一个组件需要显式地声明它所接受的props
,这样Vue才能知道外部传入的哪些是props
,那些是透传attribute
.
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
1 | <script setup> |
在没有使用 <script setup>
的组件中,props 可以使用 props
选项来声明:
1 | export default { |
注意传递给 defineProps()
的参数和提供给 props
选项的值是相同的,两种声明方式背后其实使用的都是 props 选项。
除了使用字符串数组来声明 props 外,还可以使用对象的形式:
1 | // 使用 <script setup> |
1 | // 非 <script setup> |
对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number
类型,则可使用 Number
构造函数作为其声明的值。
对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。我们将在本章节稍后进一步讨论有关 prop 校验的更多细节。
如果你正在搭配 TypeScript 使用 <script setup>
,也可以使用类型标注来声明 props:
1 | <script setup lang="ts"> |
更多关于基于类型的声明的细节请参考组件 props 类型标注。
响应式Props
解构(3.5+)
Vue的响应式系统基于属性访问跟踪状态的使用情况.例如,在计算属性或侦听器中访问props.foo
是,foo
属性将被跟踪为依赖项.
因此,在一下代码的情况下:
1 | const { foo } = defineProps(['foo']) |
在 3.4 及以下版本,foo
是一个实际的常量,永远不会改变。在 3.5 及以上版本,当在同一个 <script setup>
代码块中访问由 defineProps
解构的变量时,Vue 编译器会自动在前面添加 props.
。因此,上面的代码等同于以下代码:
1 | const props = defineProps(['foo']) |
此外,你可以使用 JavaScript 原生的默认值语法声明 props 默认值。这在使用基于类型的 props 声明时特别有用。
1 | const { foo = 'hello' } = defineProps<{ foo?: string }>() |
如果你希望在 IDE 中在解构的 props 和普通变量之间有更多视觉上的区分,Vue 的 VSCode 扩展提供了一个设置来启用解构 props 的内联提示。
将解构的props传递到函数中
当我们将解构的props传递到函数中时,例如:
1 | const { foo } defineProps(['foo']) |
这并不会按预期工作,因为它等价于watch(props.foo, ...)
,我们给watch
传递的是一个值,而不是响应式的数据源.实际上,Vue的编译器会捕捉这种情况并发出警告.
与使用watch(() => props.foo, ...)
来侦听普通props类似,我们也可以通过将其包装在getter
中来侦听解构的prop
:
1 | watch(() => foo, /* ... */) |
此外,当我们需要传递解构的 prop 到外部函数中并保持响应性时,这是推荐做法:
1 | useComposable(() => foo) |
外部函数可以调用getter
(或使用 toValue 进行规范化) 来追踪提供的 prop 变更。例如,在计算属性或侦听器的 getter 中。
传递prop的细节
Prop名字格式
如果一个prop的名字很长,应使用camelCase
,因为他们是合法的JavaScript
标识符,可以直接在模板的表达式中使用,也可以避免在作为属性key
名时必须加上引号.
1 | defineProps({ |
1 | <span>{{ greetingMessage }}</span> |
虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 内模板时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式:
1 | <MyComponent greeting-message="hello" /> |
对于组件名我们推荐使用PascalCase
,因为这提高了模板的可读性,能帮助我们区分Vue组件和原生HTML元素.然而对于传递props来说,使用camelCase
并没有太多优势,因此我们推荐更贴近HTML的书写风格.
静态 vs. 动态Props
至此,你已经见过了很多像这样的静态值形式的props
:
1 | <BlogPost title="My journey with Vue" /> |
相应地,还有使用 v-bind
或缩写 :
来进行动态绑定的 props:
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 会在控制台上向你抛出警告:
1 | const props = defineProps(['foo']) |
导致你想要更改一个 prop 的需求通常来源于以下两种场景:
- prop被用于传入初始值;而子组件想在之后将其作为一个局部数据属性.在这种情况下,最好是新定义一个局部数据属性,从props上获取初始值即可:
1 | const props = defineProps(['initialCouter']) |
- 需要对传入的prop值做进一步的转换.在这种情况中,最好是基于该prop值定义一个计算属性:
1 | const props = defineProps(['size']) |
更改对象/数组类型的props
当对象或数组作为props被传入时,虽然子组件无法更改props绑定,但仍然可以更改对象或数组内部的值.这是因为JavaScript
的对象和数组是按引用传递,对Vue来说,阻止这种更改需要付出的代价异常昂贵.
这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更加难以理解.在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合.在大多数的场景下,子组件应该抛出一个事件来通知父组件做出改变。
Prop校验
Vue组件可以更细致地声明对传入props的校验要求.比如我们上面已经看到过的类型声明,如果传入的值不满足类型的要求,Vue会在浏览器控制台中抛出警告来提醒使用者.这在开发给其他开发者使用的组件时非常有用.
要声明对props的校验,你可以向defineProps()
宏提供一个带有props校验选项的对象,例如:
1 | defineProps({ |
defineProps()
宏中的参数不可以访问<script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
一些补充细节:
- 所有props默认都是可选的,除非声明了
required: true
- 除
Boolean
外的未传递的可选prop将会有一个默认值undefined
Boolean
类型的未传递prop将被转换为false
.这可以通过为它设置default
来更改—例如:设置为default: undefiend
将与非布尔类型的prop的行为保持一致- 如果声明了
default
值,那么在prop的值被解析为undefined
时,无论prop是未被传递还是显式指明的undefined
,都会改为default
值.
当prop的校验失败后,Vue会抛出一个控制台警告(在开发模式下)
如果使用了基于类型的 prop 声明 ,Vue 会尽最大努力在运行时按照 prop 的类型标注进行编译。举例来说,defineProps<{ msg: string }>
会被编译为 { msg: { type: String, required: true }}
。
运行时类型检查
校验选项中的 type
可以是下列这些原生构造函数:
String
Number
Boolean
Array
Object
Date
Function
Symbol
Error
另外,type
也可以是自定义的类或构造函数,Vue 将会通过 instanceof
来检查类型是否匹配。例如下面这个类:
1 | class Person { |
你可以将其作为一个 prop 的类型:
1 | defineProps({ |
Vue 会通过 instanceof Person
来校验 author
prop 的值是否是 Person
类的一个实例。
可为null的类型
如果该类型是必传但可为 null 的,你可以用一个包含 null
的数组语法:
1 | defineProps({ |
注意如果 type
仅为 null
而非使用数组语法,它将允许任何类型。
Boolean类型转换
为了更贴近原生 boolean attributes
的行为,声明为 Boolean
类型的 props 有特别的类型转换规则。以带有如下声明的 <MyComponent>
组件为例:
1 | defineProps({ |
该组件可以被这样使用:
1 | <!-- 等同于传入 :disabled="true" --> |
当一个prop被声明为允许多种类型时,Boolean
的转换规则也将被应用.然而,当同时允许String
和Boolean
是,有一种边缘情况—只有当Boolean
出现在String
之前时,Boolean
的转换规则才适用:
1 | // disabled 将被转换为 true |
事件
触发与监听事件
在组件模板表达式中,可以直接使用$emit
方法触发自定义事件(例如:在v-on
的处理函数中):
1 | <!-- MyComponent --> |
父组件可以通过v-on
(缩写为@
)来监听事件.
1 | <MyComponent @some-event="callback" /> |
同样,组件的事件监听器也支持.once
修饰符
1 | <MyComponent @some-evet.onc="callback" /> |
像组件与prop一样,事件的名字也提供了自动的格式转换.注意这里我们触发了一个以camelCase
命名的事件,但是在父组件中可以使用kebab-case
形式来监听.与与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
事件参数
有时候我们会需要在触发事件时附带一个特定的值.举例来说,我们想要<BlogPost>
组件来管理文本会缩放得多大.在这个场景下,我们可以给$emit
提供一个额外的参数:
1 | <button @click="$emit('increaseBy', 1)" > |
然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数.
1 | <MyButton @increase-by="(n) => count += n" /> |
或者,也可以用一个组件方法来作为事件处理函数.
1 | <MyButton @increase-by="increaseCount" /> |
该方法也会接收到事件所传递的参数:’
1 | function increaseCount(n) { |
所有传入
$emit()
的额外参数都会被直接传向监听器。举例来说,$emit('foo', 1, 2, 3)
触发后,监听器函数将会收到这三个参数值。
声明触发的事件
组件可以显式地通过defineEmits()
宏来声明它要触发的事件:
1 | <script setup > |
我们在<template>
中使用的$emit
方法不能再组件的<script setup>
部分中使用,但defineEmits()
会返回一个相同作用的函数供我们使用:
1 | <script setup> |
defineEmits()
宏不能在子函数中使用.如上所示,它必须直接防止在<script setup>
的顶级作用域下.
如果你显式地使用了setup()
函数而不是<script setup>
,则事件需要通过 emits
选项来定义,emit
函数也被暴露在 setup()
的上下文对象上:
1 | export default { |
与 setup()
上下文对象中的其他属性一样,emit
可以安全地被解构:
1 | export default { |
这个emits
选项和defineEmits()
宏还支持对象语法.通过TypeScript
为参数指定类型,它允许我们对触发事件的参数进行验证:
1 | <script setup lang="ts" > |
如果你正在搭配TypeScript
使用<script setup>
,也可以使用纯类型标注来声明触发事件:
1 | <script setup lang="ts"> |
TypeScript 用户请参考:如何为组件所抛出事件标注类型
尽管事件声明是可选的,我们还是推荐你完整地声明所有要触发的事件,以此在代码中作为文档记录组件的用法。同时,事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况。
如果一个原生事件的名字 (例如
click
) 被定义在emits
选项中,则监听器只会监听组件触发的click
事件而不会再响应原生的click
事件。
事件校验
和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit
的内容,返回一个布尔值来表明事件是否合法。
1 | <script setup> |
组件v-model
基本用法
v-model
可以在组件上使用以实现双向绑定
从3.4开始,推荐的实现方式是使用defineModel()
宏
1 | <!-- Child.vue --> |
父组件可以使用v-modle
绑定一个值
1 | <!-- Parent.vue --> |
defineModel()
的返回值是一个ref
.它可以像其他ref
一样被访问以及修改,不过它能起到在父组件和当前变量之间的双向绑定作用:
- 它的
.value
和父组件的v-model
的值同步 - 当它被子组件变更了,会触发父组件绑定的值一起更新
这意味着你也可以用v-model
把这个ref
绑定到一个原生的input
元素上,在提供相同的v-model
用法的同时轻松包装原生input
元素:
1 | <script setup> |
底层机制
defineModel
是一个便利宏.编译器将其展开为以下内容:
- 一个名为
modelValue
的prop,本地ref值与其同步 - 一个名为
update:modelValue
的事件,当本地ref
值发生变更时触发.
在 3.4 版本之前,你一般会按照如下的方式来实现上述相同的子组件:
1 | <!-- Child.vue --> |
然后父组件中的v-model="foo"
将被编译为
1 | <!-- Parent.vue --> |
如你所见,这显得冗长得多。然而,这样写有助于理解其底层机制。
因为 defineModel
声明了一个 prop,你可以通过给 defineModel
传递选项,来声明底层 prop 的选项:
1 |
|
如果为
defineModel
prop 设置了一个default
值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步。在下面的示例中,父组件的myRef
是 undefined,而子组件的model
是 1:子组件:
1 const model = defineModel({default: 1})父组件:
1 const myRef = ref()
1 <Child v-model="myRef"></Child>
v-model
的参数
组件上的v-model
也可以接受一个参数:
1 | <MyComponent v-model:title="bookTitle" /> |
在子组件中,我们可以通过将字符串作为第一个参数传递给defineModel()
来支持相应的参数:
1 | <!-- MyComponent.vue --> |
如果需要额外的 prop 选项,应该在 model 名称之后传递:
1 | const title = defineModel('title', { required: true }) |
多个v-model
的绑定
利用刚才在 v-model
的参数小节中学到的指定参数与事件名的技巧,我们可以在单个组件实例上创建多个 v-model
双向绑定。
组件上的每一个 v-model
都会同步不同的 prop,而无需额外的选项:
1 | <UserName |
1 | <script setup> |
处理v-model
修饰符
在学习输入绑定时,我们知道了 v-model
有一些内置的修饰符,例如 .trim
,.number
和 .lazy
。在某些场景下,你可能想要一个自定义组件的 v-model
支持自定义的修饰符。
我们来创建一个自定义的修饰符 capitalize
,它会自动将 v-model
绑定输入的字符串值第一个字母转为大写:
1 | <MyComponent v-model.capitalize="myText" /> |
通过像这样解构defineModel()
的返回值,可以在子组件中访问添加到组件v-model
的修饰符.
1 | <script setup> |
为了能够基于修饰符选择性地调节值的读取和写入方式,我们可以给 defineModel()
传入 get
和 set
这两个选项。这两个选项在从模型引用中读取或设置值时会接收到当前的值,并且它们都应该返回一个经过处理的新值。下面是一个例子,展示了如何利用 set
选项来应用 capitalize
(首字母大写) 修饰符:
1 | <script setup> |
带参数的v-model
修饰符
这里是另一个例子,展示了如何在使用多个不同参数的v-model
时使用修饰符:
1 | <UserName |
1 | <script setup> |