Vue3.js(组合式API)

Vue3.js(组合式API)
LiSirvue脚手架
- @vue/cil是vue官方提供的一个全局模块包(得到vue命令),此包用来创建vue脚手架项目
- 脚手架目录和代码分析
vue安装全局包
全局安装@vue/cil模块包
1 | yarn global add @vue/cli |
查看是否安装成功
@vue/cli创建项目
1.创建项目
1 | # 创建项目 |
注意:项目名字中不能有大写字母。中文和特殊符号
2.开始创建,成功如下
3.进入项目文件输入yarn serve启动项目,浏览器输出如下代表成功
项目结构
创建一个 Vue3应用
API——setup函数
setup是Vue3中的一个新的配置项,值是一个函数,他是组合式API”表演的舞台”,组件中所用到所有数据,方法、计算属性、监视….等等,均配置在setup中
基本使用
setup() 钩子是在组件中使用组合式 API 的入口,通常只在以下情况下使用:
- 需要在非单文件组件中使用组合式 API 时。
- 需要在基于选项式 API 的组件中集成基于组合式 API 的代码时。
注意
对于结合单文件组件使用的组合式 API,推荐通过 `` 以获得更加简洁及符合人体工程学的语法。
我们可以使用响应式 API 来声明响应式的状态,在 setup() 函数中返回的对象会暴露给模板和组件实例。其他的选项也可以通过组件实例来获取 setup() 暴露的属性:
1 | <script> |
setup() 自身并不含对组件实例的访问权,即在 setup() 中访问 this 会是 undefined。你可以在选项式 API 中访问组合式 API 暴露的值,但反过来则不行。
setup语法糖
对于setup函数,我们最基本的用法是
1 | setup(){ |
现在我引入setup语法糖,写法变得很简洁简单
1 | <script setup> |
那么上面这种写法,就相当于最基本使用的写法,可以看到简洁了不少
应用实例
每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例:
1 | import { createApp } from 'vue' |
根组件
我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。
如果你使用的是单文件组件,我们可以直接从另一个文件中导入根组件。
1 | import { createApp } from 'vue' |
挂载应用
应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:
1 | <div id="app"></div> |
1 | app.mount('#app') |
应用根组件的内容将会被渲染在容器元素里面。容器元素自己将不会被视为应用的一部分
.mount() 方法应该始终在整个应用配置和资源注册完成后被调用。同时请注意,不同于其他资源注册方法,它的返回值是根组件实例而非应用实例。
模板语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。
在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
如果你对虚拟 DOM 的概念比较熟悉,并且偏好直接使用 JavaScript,你也可以结合可选的 JSX 支持直接手写渲染函数而不采用模板。但请注意,这将不会享受到和模板同等级别的编译时优化。
文本插值
最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 (即双大括号):
template
1 | <span>Message: {{ msg }}</span> |
双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。
原始 HTML
双大括号会将数据解释为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令:
1 | <script setup> |
1 | //而使用vue中的{{}}不会渲染html,所以会显示纯文本 |
这里我们遇到了一个新的概念。这里看到的 v-html attribute 被称为一个指令。指令由 v- 作为前缀,表明它们是一些由 Vue 提供的特殊 attribute,你可能已经猜到了,它们将为渲染的 DOM 应用特殊的响应式行为。这里我们做的事情简单来说就是:在当前组件实例上,将此元素的 innerHTML 与 rawHtml 属性保持同步。
span 的内容将会被替换为 rawHtml 属性的值,插值为纯 HTML——数据绑定将会被忽略。注意,你不能使用 v-html 来拼接组合模板,因为 Vue 不是一个基于字符串的模板引擎。在使用 Vue 时,应当使用组件作为 UI 重用和组合的基本单元。
Attribute 绑定
双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令:
1 | <div v-bind:id="dynamicId"></div> |
v-bind 指令指示 Vue 将元素的 id attribute 与组件的 dynamicId 属性保持一致。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。
简写
因为 v-bind 非常常用,我们提供了特定的简写语法:
1 | <div :id="dynamicId"></div> |
开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。此外,他们不会出现在最终渲染的 DOM 中。简写语法是可选的,但相信在你了解了它更多的用处后,你应该会更喜欢它。
同名简写 (要求vue版本为3.4+)
如果 attribute 的名称与绑定的 JavaScript 值的名称相同,那么可以进一步简化语法,省略 attribute 值:
1 | <!-- 与 :id="id" 相同 --> |
这与在 JavaScript 中声明对象时使用的属性简写语法类似。请注意,这是一个只在 Vue 3.4 及以上版本中可用的特性。
布尔型 Attribute
布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。
v-bind 在这种场景下的行为略有不同:
1 | <button :disabled="isButtonDisabled">Button</button> |
当 isButtonDisabled 为真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:
1 | const objectOfAttrs = { id: 'container', class: 'wrapper' } |
通过不带参数的 v-bind,你可以将它们绑定到单个元素上:
1 | <div v-bind="objectOfAttrs"></div> |
使用 JavaScript 表达式
至此,我们仅在模板中绑定了一些简单的属性名。但是 Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:
1 | {{ number + 1 }} |
这些表达式都会被作为 JavaScript ,以当前组件实例为作用域解析执行。
在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:
- 在文本插值中 (双大括号)
- 在任何 Vue 指令 (以
v-开头的特殊 attribute) attribute 的值中
仅支持表达式
每个绑定仅支持单一表达式,也就是一段能够被求值的 JavaScript 代码。一个简单的判断方法是是否可以合法地写在 return 后面。
1 | <!-- 这是一个语句,而非表达式 --> |
调用函数
可以在绑定的表达式中使用一个组件暴露的方法:
1 | <time :title="toTitleDate(date)" :datetime="date"> |
指令 Directives
指令是带有 v- 前缀的特殊 attribute。Vue 提供了许多内置指令,包括上面我们所介绍的 v-bind 和 v-html。
指令 attribute 的期望值为一个 JavaScript 表达式 (除了少数几个例外,即之后要讨论到的 v-for、v-on 和 v-slot)。一个指令的任务是在其表达式的值变化时响应式地更新 DOM。以 v-if 为例:
1 | <p v-if="seen">Now you see me</p> |
这里,v-if 指令会基于表达式 seen 的值的真假来移除/插入该 <p> 元素。
参数 Arguments
某些指令会需要一个“参数”,在指令名后通过一个冒号隔开做标识。例如用 v-bind 指令来响应式地更新一个 HTML attribute:
1 | <a v-bind:href="url"> ... </a> |
这里 href 就是一个参数,它告诉 v-bind 指令将表达式 url 的值绑定到元素的 href attribute 上。在简写中,参数前的一切 (例如 v-bind:) 都会被缩略为一个 : 字符。
另一个例子是 v-on 指令,它将监听 DOM 事件:
1 | <a v-on:click="doSomething"> ... </a> |
这里的参数是要监听的事件名称:click。v-on 有一个相应的缩写,即 @ 字符。我们之后也会讨论关于事件处理的更多细节。
动态参数
同样在指令参数上也可以使用一个 JavaScript 表达式,需要包含在一对方括号内:
1 | <!-- |
这里的 attributeName 会作为一个 JavaScript 表达式被动态执行,计算得到的值会被用作最终的参数。举例来说,如果你的组件实例有一个数据属性 attributeName,其值为 "href",那么这个绑定就等价于 v-bind:href。
相似地,你还可以将一个函数绑定到动态的事件名称上:
1 | <a v-on:[eventName]="doSomething"> ... </a> |
在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus。
1 | <a v-on:[eventName]="doSomething"> ... </a> |
在此示例中,当 eventName 的值是 "focus" 时,v-on:[eventName] 就等价于 v-on:focus。
动态参数值的限制
动态参数中表达式的值应当是一个字符串,或者是 null。特殊值 null 意为显式移除该绑定。其他非字符串的值会触发警告。
动态参数语法的限制
动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的。例如下面的示例:
1 | <!-- 这会触发一个编译器警告 --> |
如果你需要传入一个复杂的动态参数,我们推荐使用计算属性替换复杂的表达式,也是 Vue 最基础的概念之一,我们很快就会讲到。
当使用 DOM 内嵌模板 (直接写在 HTML 文件里的模板) 时,我们需要避免在名称中使用大写字母,因为浏览器会强制将其转换为小写:
1 | <a :[someAttr]="value"> ... </a> |
上面的例子将会在 DOM 内嵌模板中被转换为 :[someattr]。如果你的组件拥有 “someAttr” 属性而非 “someattr”,这段代码将不会工作。单文件组件内的模板不受此限制。
修饰符 Modifiers
修饰符是以点开头的特殊后缀,表明指令需要以一些特殊的方式被绑定。例如 .prevent 修饰符会告知 v-on 指令对触发的事件调用 event.preventDefault()来阻止默认行为:
1 | <form @submit.prevent="onSubmit">...</form> |
之后在讲到 v-on 和 v-model 的功能时,你将会看到其他修饰符的例子。
最后,在这里你可以直观地看到完整的指令语法:
响应式基础
声明响应式状态
ref()
在组合式 API 中,推荐使用 ref() 函数来声明响应式状态:
语法:let xxx = ref(初始值)
1 | //引入ref函数 |
ref() 接收参数,并将其包裹在一个带有 .value 属性的 ref 对象中返回:
1 | const count = ref(0) |
要在组件模板中访问 ref,请从组件的 setup() 函数中声明并返回它们:
1 | import { ref } from 'vue' |
1 | <div>{{ count }}</div> |
注意:
- 在模板中使用 ref 时,我们不需要附加
.value。为了方便起见,当在模板中使用时,ref 会自动解包 - 在
JS中操作模板中的数据时,需要xxx.value,而模板直接使用即可 - 比如对于 let name = ref(‘张三’)来说,name 不是响应式的,name.value 是响应式的。
- ref定义的是基本类型数据,也可以定义数据类型
reactive()
特别强调:reactive()只能定义对象类型响应式数据
还有另一种声明响应式状态的方式,即使用 reactive() API。与将内部值包装在特殊对象中的 ref 不同,reactive() 将使对象本身具有响应性:
1 | <script setup> |
在模板中使用:
1 | <div class="test"> |
ref()对比reactive()
reactive() 的局限性
reactive() API 有一些局限性:
- 有限的值类型:它只能用于对象类型 (对象、数组和如
Map、Set这样的集合类型)。它不能持有如string、number或boolean这样的原始类型。 - 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用。这意味着我们不能轻易地“替换”响应式对象,因为这样的话与第一个引用的响应性连接将丢失
- 对解构操作不友好:当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接
由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API。
toRefs和toRef
toRefs可以把一个由reactive代理的响应式对象里面的每一个Key-value变成由ref代理的响应式数据
图解:
声明方法
要为组件添加方法,我们需要用到 methods 选项。它应该是一个包含所有方法的对象:
1 | export default { |
Vue 自动为 methods 中的方法绑定了永远指向组件实例的 this。这确保了方法在作为事件监听器或回调函数时始终保持正确的 this。你不应该在定义 methods 时使用箭头函数,因为箭头函数没有自己的 this 上下文。
1 | export default { |
和组件实例上的其他属性一样,方法也可以在模板上被访问。在模板中它们常常被用作事件监听器:
1 | <button @click="increment">{{ count }}</button> |
在上面的例子中,increment 方法会在 <button> 被点击时调用。
深层响应性
在 Vue 中,默认情况下,状态是深度响应的。这意味着当改变嵌套对象或数组时,这些变化也会被检测到:
1 | export default { |
DOM 更新时机
当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:
1 | import { nextTick } from 'vue' |
有状态方法
在某些情况下,我们可能需要动态地创建一个方法函数,比如创建一个预置防抖的事件处理器:
1 | import { debounce } from 'lodash-es' |
不过这种方法对于被重用的组件来说是有问题的,因为这个预置防抖的函数是有状态的:它在运行时维护着一个内部状态。如果多个组件实例都共享这同一个预置防抖的函数,那么它们之间将会互相影响。
要保持每个组件实例的防抖函数都彼此独立,我们可以改为在 created 生命周期钩子中创建这个预置防抖的函数:
1 | export default { |
计算属性
基础示例
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
1 | const author = reactive({ |
我们想根据 author 是否已有一些书籍来展示不同的信息:
1 | <p>Has published books:</p> |
这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于 author.books。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
1 | <script setup> |
1 | <p>Has published books:</p> |
我们在这里定义了一个计算属性 publishedBooksMessage。
更改此应用的 data 中 books 数组的值后,可以看到 publishedBooksMessage 也会随之改变。
在模板中使用计算属性的方式和一般的属性并无二致。Vue 会检测到 this.publishedBooksMessage 依赖于 this.author.books,所以当 this.author.books 改变时,任何依赖于 this.publishedBooksMessage 的绑定都将同时更新。
计算属性缓存 vs 方法
你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:
1 | <p>{{ calculateBooksMessage() }}</p> |
1 | // 组件中 |
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。
这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:
js
1 | const now = computed(() => Date.now()) |
Class 与 Style 绑定
数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 class 和 style 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 class 和 style 的 v-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。
绑定 HTML class
绑定对象
我们可以给 :class (v-bind:class 的缩写) 传递一个对象来动态切换 class:
1 | <div :class="{ active: isActive }"></div> |
上面的语法表示 active 是否存在取决于数据属性 isActive 的真假值。
你可以在对象中写多个字段来操作多个 class。此外,:class 指令也可以和一般的 class attribute 共存。举例来说,下面这样的状态:
1 | const isActive = ref(true) |
1 | <div |
这将渲染:
1 | <div class="static active"></div> |
当 isActive 或者 hasError 改变时,class 列表会随之更新。举例来说,如果 hasError 变为 true,class 列表也会变成 "static active text-danger"。
还可以写成下面这样
1 | const classObject = reactive({ |
这将渲染:
1 | <div class="active"></div> |
我们也可以绑定一个返回对象的计算属性。这是一个常见且很有用的技巧:
1 | data() { |
1 | <div :class="classObject"></div> |
绑定数组
我们可以给 :class 绑定一个数组来渲染多个 CSS class:
1 | data() { |
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> |
在组件上使用
对于只有一个根元素的组件,当你使用了 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> |
绑定内联样式
绑定对象
:style 支持绑定 JavaScript 对象值,对应的是 HTML 元素的 style 属性:
1 | data() { |
1 | <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> |
直接绑定一个样式对象通常是一个好主意,这样可以使模板更加简洁:
1 | data() { |
同样的,如果样式对象需要更复杂的逻辑,也可以使用返回样式对象的计算属性。
绑定数组
我们还可以给 :style 绑定一个包含多个样式对象的数组。这些对象会被合并后渲染到同一元素上:
1 | <!-- 测试绑定数组 --> |
1 | data(){ |
自动前缀
当你在 :style 中使用了需要浏览器特殊前缀的 CSS 属性时,Vue 会自动为他们加上相应的前缀。Vue 是在运行时检查该属性是否支持在当前浏览器中使用。如果浏览器不支持某个属性,那么将尝试加上各个浏览器特殊前缀,以找到哪一个是被支持的。
样式多值
1 | <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div> |
数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex。
条件渲染
v-if
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
v-else
你也可以使用 v-else 为 v-if 添加一个“else 区块”。
1 | <script> |
注意:一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。
v-else-if
顾名思义,v-else-if 提供的是相应于 v-if 的“else if 区块”。它可以连续多次重复使用:
1 | <div v-if="type === 'A'"> |
和 v-else 类似,一个使用 v-else-if 的元素必须紧跟在一个 v-if 或一个 v-else-if 元素后面。
<template> 上的 v-if
1 | <template v-if="ok"> |
1 | <script> |
v-else 和 v-else-if 也可以在 <template> 上使用。
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-if 和 v-for
当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。
提醒:不推荐同时使用v-if 和 v-for二者优先级不同
列表渲染
v-for
我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是要渲染的数组,而 item 是迭代项的别名:
1 | data() { |
1 | <li v-for="item in items"> |
完整代码
1 | <script> |
v-for 变量的作用域和下面的 JavaScript 代码很类似:
1 | const parentMessage = 'Parent' |
上面说明:V-for这个指令中的变量只能在该指令内使用。
注意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:
1 | <li v-for="{ message } in items"> |
对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:
1 | <li v-for="item in items"> |
span标签的v-for是li标签v-for的儿子,所以span中可以访问到li标签中的item
你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:
1 | <div v-for="item of items"></div> |
v-for 与对象
你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。
1 | data() { |
1 | <ul> |
看下效果
可以通过提供第二个参数表示属性名 (例如 key):
1 | <li v-for="(value, key) in myObject"> |
看下效果
第三个参数表示位置索引:
1 | <li v-for="(value, key, index) in myObject"> |
看下效果
在 v-for 里使用范围值
v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。
1 | <span v-for="n in 102">{{ n }}</span> |
注意此处 n 的初值是从 1 开始而非 0。
<template> 上的 v-for
与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:
1 | <script> |
v-for 与 v-if
当它们同时存在于一个节点上时,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 属性:
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"> |
推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。
key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。
组件上使用 v-for
我们可以直接在组件上使用 v-for,和在一般的元素上使用没有区别 (别忘记提供一个 key):
1 | <MyComponent v-for="item in items" :key="item.id" /> |
但是,这不会自动将任何数据传递给组件,因为组件有自己独立的作用域。为了将迭代后的数据传递到组件中,我们还需要传递 props:
1 | <todo-item |
在TodoItem组件中使用props来接收titile并渲染
1 | <script> |
数组变化侦测
操作数组的方法
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push():向数组的末尾添加一个或多个元素,并返回新的长度。pop():删除数组中的最后一个元素,并返回该元素的值。shift():删除数组中的第一个元素,并返回该元素的值。unshift():在数组的开头添加一个或多个元素,并返回新的长度。splice():这个方法可以用于删除、插入或替换数组中的元素。它接受至少两个参数:第一个参数是要操作的起始位置的索引,第二个参数是要删除的元素数量。可选的第三个参数及以后是你要插入到指定位置的新元素。sort():对数组的元素进行排序,默认情况下按照字符串的Unicode码点顺序排序。如果你想自定义排序逻辑,可以传入一个比较函数作为参数。这里说明比较函数通常是下面这种写法
1
2
3
4
5
6
7
8//升序
(a,b)=>{
return a-b
}
//降序
(a,b)=>{
return b-a
}
reverse():反转数组中元素的顺序,第一个元素变成最后一个,最后一个变成第一个。
替换一个数组
变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter(),concat() 和 slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:
filter() 方法创建一个新的数组,其包含通过所提供函数实现的测试的所有元素。
1 | const numbers = [1, 2, 3, 4, 5, 6]; |
concat() 方法用于合并两个或多个数组。此方法不会更改现有的数组,而是返回一个新的数组。
1 | const array1 = ['a', 'b']; |
slice() 方法返回一个新的数组对象,它是原数组的一部分。你可以指定开始和结束的位置来截取数组的一部分,不会改变原数组。
1 | const array = ['a', 'b', 'c', 'd', 'e']; |
展示过滤或排序后的结果
有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。
1 | data() { |
1 | <li v-for="n in evenNumbers">{{ n }}</li> |
在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),你可以使用以下方法:
1 | data() { |
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 | <script setup> |
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。
1 | <script setup> |
方法事件处理器会自动接收原生 DOM 事件并触发执行。在上面的例子中,我们能够通过被触发事件的 event.target 访问到该 DOM 元素。
方法与内联事件判断
模板编译器会通过检查 v-on 的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo、foo.bar 和 foo['bar'] 会被视为方法事件处理器,而 foo() 和 count++ 会被视为内联事件处理器。
在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
1 | <script> |
在内联事件处理器中访问事件参数
1 | <!-- 使用特殊的 $event 变量 --> |
1 | methods: { |
事件修饰符
在处理事件时调用 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 | <!--@keyup时键盘抬起事件,"page-down"指的是键盘上的pageDown按键,意味着按下pageDown触发事件,抬起时触发事件onPageDown--> |
在上面的例子中,仅会在 $event.key(指按键) 为 'PageDown' 时调用事件处理。
按键别名
.enter:回车.tab:Tab键.delete(捕获“Delete”和“Backspace”两个按键).esc: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 键将不会触发。
- 比如我们的
ctrl c,ctrl v相信这么一说就都明白了。 - 通过
@keyup.ctrl.c和@keyup.ctrl.v来绑定
.exact 修饰符
.exact 修饰符允许精确控制触发事件所需的系统修饰符的组合。
1 | <script> |
鼠标按键修饰符
.left:默认情况下,如果不使用任何修饰符,Vue.js的鼠标事件监听器会响应所有的鼠标按钮。但是,.left修饰符确保只有在左键被按下或释放时才触发事件处理函数。.right:使用.right修饰符,事件处理函数仅在右键被按下或释放时触发。这对于创建上下文菜单或其他依赖右键操作的交互特别有用。.middle:.middle修饰符则确保只有在鼠标中键(通常是滚轮按钮)被按下或释放时才触发事件处理函数。这在需要监听滚轮点击的场景中很有用。
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
比如下面这样:
1 | <script> |
表单输入绑定
在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:
1 | <input |
v-model 指令帮我们简化了这一步骤:
1 | <input v-model="text"> |
另外,v-model 还可以用于各种不同类型的输入,<textarea>、<select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:
- 文本类型的
<input>和<textarea>元素会绑定valueproperty 并侦听input事件; <input type="checkbox">(复选框) 和<input type="radio">(单选框) 会绑定checkedproperty 并侦听change事件;<select>会绑定valueproperty 并侦听change事件。
注意:
v-model 会忽略任何表单元素上初始的 value、checked 或 selected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用data 选项来声明该初始值。
也就是说,使用v-model绑定某个值时,该值必须在data函数中声明并返回给vue模板
基本用法
来看下面这段代码
1 | <script> |
效果
多行文本
同样的道理,来看下面这段代码
1 | <span>Multiline message is:</span> |
效果和上面的是一样的
注意:
<textarea> 中是不支持插值表达式的。请使用 v-model 来替代:
1 | <!-- 错误 --> |
复选框
单一的复选框,绑定布尔类型值:
1 | <script setup> |
我们也可以将多个复选框绑定到同一个数组或集合的值:
1 | <script setup> |
1 | <template> |
页面效果
在这个例子中,checkedNames 数组将始终包含所有当前被选中的框的value属性对应的值。关于value绑定,请详见值绑定小结
单选按钮
1 | <script setup> |
上面这段代码同样,效果也一样
选择器
单个选择器的示例如下:
1 | <div>Selected: {{ selected }}</div> |
页面效果
注意
如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项,如上面的例子所示。
多选 (值绑定到一个数组):
只是比单选多个一个multiple属性
1 | <div>Selected: {{ selected }}</div> |
选择器的选项可以使用 v-for 动态渲染:
1 | export default { |
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() 处理,那么将返回原始值。
number 修饰符会在输入框有 type="number" 时自动启用。也就是说效果等同于下面这么写
1 | <input v-model="age" type="number"/> |
parseFloat(),用于解析一个字符串并返回一个浮点数。如果字符串的开始部分不是一个有效的浮点数表示,则返回 NaN(Not-a-Number)。
下面是一组数据,能够更直观的感受该函数的用法
1 | console.log(parseFloat("123.45")); // 输出:123.45 |
.trim
如果你想要默认自动去除用户输入内容中两端的空格,你可以在 v-model 后添加 .trim 修饰符:
1 | <input v-model.trim="msg" /> |
生命周期钩子
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
注册周期钩子
举例来说,mounted 钩子可以用来在组件完成初始渲染并创建 DOM 节点后运行代码:
1 | export default { |
还有其他一些钩子,会在实例生命周期的不同阶段被调用,最常用的是 mounted、updated 和 unmounted。
所有生命周期钩子函数的 this 上下文都会自动指向当前调用它的组件实例。注意:避免用箭头函数来定义生命周期钩子,因为如果这样的话你将无法在函数中通过 this 获取组件实例。
生命周期图示
下面是实例生命周期的图表。你现在并不需要完全理解图中的所有内容,但以后它将是一个有用的参考。
生命周期选项
注意:使用生命周期钩子时,要手动引入对应的钩子,然后将回调函数当成参数写在钩子里面
onBeforeMount()
注册一个钩子,在组件被挂载之前被调用。
详细信息
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
这个钩子在服务端渲染时不会被调用。
1 | import {onBeforeMount} from 'vue' |
onMounted
在组件被挂载之后调用。
详细信息
组件在以下情况下被视为已挂载:
- 所有同步子组件都已经被挂载。(不包含异步组件或
<Suspense>树内的组件) - 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端被调用。
这个钩子在服务端渲染时不会被调用。
1
2
3
4import {onMounted} from 'vue'
onMounted(()=>{
//处理的逻辑
})- 所有同步子组件都已经被挂载。(不包含异步组件或
onBeforeUpdate
在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
详细信息
这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
这个钩子在服务端渲染时不会被调用。
1
2
3
4import {onBeforeUpdate} from 'vue'
onBeforeUpdate(()=>{
//逻辑
})
onUpdated
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
详细信息
父组件的更新钩子将在其子组件的更新钩子之后调用。
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
这个钩子在服务端渲染时不会被调用。
警告:
不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
1 | import {onUpdated} from 'vue' |
onBeforeUnmount
在一个组件实例被卸载之前调用。
详细信息
当这个钩子被调用时,组件实例依然还保有全部的功能。
这个钩子在服务端渲染时不会被调用。
1
2
3
4import {onBeforeUnmount} from 'vue'
onBeforeUnmount(()=>{
//逻辑
})
onUnmounted
在一个组件实例被卸载之后调用。
详细信息
一个组件在以下情况下被视为已卸载:
- 其所有子组件都已经被卸载。
- 所有相关的响应式作用 (渲染作用以及
setup()时创建的计算属性和侦听器) 都已经停止。
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务端渲染时不会被调用。
1
2
3
4import {onUnmounted} from 'vue'
onUnmounted(()=>{
//逻辑
})
侦听器(watch)
基本示例
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。
在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:
下面示例一段代码,来展示watch的用法
1 | <script setup> |
侦听数据源类型
watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象(reactive定义的响应式对象)、一个 getter 函数、或多个数据源组成的数组,说白了就是下面这四种情况:
监听ref定义的基本数据类型
1 | <script setup> |
监视ref定义的对象类型数据
1 | <script setup> |
注意:
针对于nV和oV讲讲:当你仅仅是对对象内的属性值做出了修改时,通过监听获得nV和oV这两个值是相同的,当你完全改变了这个对象包括对象的名字和属性时,这时监听所获得的nV和oV是不同的,即新值和旧值,本质是因为当你仅仅改变对象内的属性值时,还是同一个对象,对象存储的地址没有发生改变。
监视reactive定义的对象类型数据
使用reactive定义的对象,是默认开启深度监视的,所以直接监视即可。
1 | <script setup> |
监听reactive或ref中的对象类型的属性值
注意,你不能直接侦听响应式对象的属性值,例如:
1 | const obj = reactive({ count: 0 }) |
如果想监听响应式对象的属性值,需要用一个返回该属性值的getter函数,getter简单来说就是一个函数,返回一个值
1 | // 提供一个 getter 函数 |
深层侦听器
直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:
来看代码
1 | const obj = reactive({ count: 0 }) |
相比之下,一个返回响应式对象的 getter 函数,只有在返回不同的对象时,才会触发回调:
1 | watch( |
你也可以给上面这个例子显式地加上 deep 选项,强制转成深层侦听器:
1 | watch( |
提醒
深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。
即时回调的侦听器
watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。举例来说,我们想请求一些初始数据,然后在相关状态更改时重新请求数据。
我们可以用一个对象来声明侦听器,给其添加immediate: true 选项,这样便能强制回调函数立即执行:
来看代码
1 | watch( |
一次性侦听器(vue版本3.4+)
每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。
1 | watch( |
watchEffect()
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
1 | watchEffect(async () => { |
这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。
watch vs. watchEffect
watch只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
回调的触发时机
当你更改了响应式状态,它可能会同时触发 Vue 组件更新和侦听器回调。
类似于组件更新,用户创建的侦听器回调函数也会被批量处理以避免重复调用。例如,如果我们同步将一千个项目推入被侦听的数组中,我们可能不希望侦听器触发一千次。
默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。这意味着如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。
Post Watchers
如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,你需要指明 flush: 'post' 选项:
1 | watch(source, callback, { |
同步侦听器
你还可以创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发:设置属性flush:'sync'
1 | watch(source, callback, { |
停止侦听器
在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,你无需关心怎么停止一个侦听器
一个关键点是,侦听器必须用同步语句创建:如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。如下方这个例子:
1 | <script setup> |
要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数:
1 | const unwatch = watchEffect(() => {}) |
模板引用
虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:
1 | <input ref='input'/> |
ref 是一个特殊的 属性,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。
访问模板引用
1 | <script setup> |
注意,你只可以在组件挂载后才能访问模板引用。如果你想在模板中的表达式上访问 $refs.input,在初次渲染时会是 undefined。这是因为在初次渲染前这个元素还不存在呢!
v-for 中的模板引用
需要 v3.2.25 及以上版本
当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素
1 | <script setup> |
应该注意的是,ref 数组并不保证与源数组相同的顺序。
函数模板引用
除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:
1 | <input :ref="(el) => { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> |
注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。
组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:
这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
定义一个组件
当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):
1 | <script setup> |
当不使用构建步骤时,一个 Vue 组件以一个包含 Vue 特定选项的 JavaScript 对象来定义:
1 | import { ref } from 'vue' |
这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源。
上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。
使用组件
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式(使用export deafult{})被暴露给外部。
1 | <script setup> |
通过 <script setup>,导入的组件都在模板中直接可用。需要在 components 选项上注册它。这个组件将会以其注册时的名字作为模板中的标签名。
当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。
组件可以被重用任意多次:
1 | <h1>Here is a child component!</h1> |
组件通信
通过props来传递数据
这里假设我有两个组件,一个称为父组件,另一个是子组件
Props 是一种特别的 属性,你可以在组件上声明注册。这里要用到 props 选项:
父组件向子组件传递信息
1 | <script setup> |
在父组件中,需这样书写。
1 | <BlogPost title="My journey with Vue" /> |
也可以通过动态绑定的形式,传递数据。一般来说,我们在子组件中声明接收某个变量时,那么就在父组件中可以动态绑定该变量即可
子组件向父组件传递数据
原理:本质上还是父传子,只不过这时父组件需要给子组件传递一个函数,在子组件中调用父组件传递过来的函数并传入参数(子向父传递的数据),相当于回调
通过自定义事件(子传父)来传递数据
自定义事件是我们自己来编写的,不同于点击,鼠标移入移出等事件,对于这个应用场景,下面来这样解释
首先在父组件中编写自定义事件
子组件中通过函数
defineEmits来声明在父组件中的自定义事件触发事件
const emit = defineEmits(['CustomEvent'])- 使用
emit来触发事件,语法是emit('要触发的事件名称') - 图解:
使用mitt进行组件通信
安装mitt
1 | yarn add mitt |
编辑mitt
1 | //引入mitt |
使用mitt
mitt(): 这个构造函数返回一个新的事件发射器实例。on(eventName, handler): 订阅特定事件。当事件触发时,对应的处理器(handler)将被调用。eventName可以是任何字符串,handler应该是一个函数。emit(eventName, ...args): 触发一个事件。这会调用所有已注册到该事件的处理器。eventName是事件的名称,...args是传递给处理器的任意参数。off(eventName, handler): 取消订阅特定事件的处理器。如果没有提供处理器,则会取消订阅所有该事件的处理器。all: 这是一个特殊属性,允许你订阅所有事件。当你订阅all时,提供的处理器会接收到所有触发的事件以及它们的名称。once(eventName, handler):订阅一次性的事件。处理器会在事件第一次触发时调用,然后自动取消订阅。
案例:
使用v-model进行组件传值(v-model双向绑定实现的原理) - 面试重点
v-model用在html标签上
通过v-model实现数据与视图的双向绑定
<input type="text" v-model="username"/>1
2
3
4
5
6
7
2. 通过v-model的底层原理实现双向绑定
1. ```html
<input :value="username"
@input="username = <HTMLInputElement>($event.target).value"
/>:value实现数据到视图的绑定,而@input="username = $event.target.value"实现视图到数据的绑定,实际上是通过$event事件对象来拿到更改输入框中的值,将其赋值给username
v-model用在组件标签上
业务场景:封装一个组件,实现双向绑定的功能
上面展示的就是v-model的底层原理,一般图片右边的工作是UI组件库所要做的,类似于我们直接调用UI组件库,在组件上使用属性v-model来实现的数据绑定,实际上就是我们自定义这样来实现的。
- 上面图片中的事件名称格式是:
@update:xxxx。 - 对于上面的形式,我们在自定义组件实现双向绑定功能时,左边图片就可以简写成
v-model=username,而右边是我们真正需要手动实现的内容,来实现双向绑定,我想要说的是,v-model=username就帮助我们实现了左边组件的写法了,我们只需要实现右边的就可以了。 - 实际上v-model还可以起名字,写法
v-model:xxx,如果写成这样的话,那么
在拓展一下$event什么时候有.target
- 绑定在html标签上的事件,有
- 绑定在自定义组件上的事件,没有,并且
$event就相当于传递的数据
使用$attrs来传递数据
适用场景:
当我们要实现从A中向C中传递数据时,使用
$attrs,实现当前组件的父组件(A组件),给当前组件的子组件(C组件)传递数据实现:在B组件中,给C组件绑定一个属性
v-bind='$attrs'即可该三个组件属于内嵌关系,A组件中使用了B组件,B组件中使用了C组件
数据的流向实际上是:A组件(父组件中定义的数据)—->B组件—–>B组件通过在C组件上绑定
v-bind=$attrs——>孙组件(C组件)收到了来自A组件的数据。这样实际上是爷——>孙组件之间的通信。爷组件中定义函数,孙组件中调用函数,作为一种回调的形式,实现孙—–>爷的数据传递
$ref和$parent
- $ref用于父传子
- $parent用与子传父
| 属性 | 说明, |
|---|---|
| $ref | 值是对象,包含所有被ref属性表示的DOM元素或组件实例。 |
| $parent | 值为对象,当前组件的父组件实例对象。 |
注意:在必要时,使用宏函数defineExpose({})把要暴露出的东西作为对象。用来解决数据没有拿到的问题
使用provide和inject实现组件通信
提供数据的组件使用provide来提供数据
语法:
1 | import {provide, ref} from 'vue' |
接收数据的组件通过inject来注入数据
语法:
1 | <template> |
这样就完成了双方的通信。
组件通信总结
监听事件
在父组件中,我们可以添加一个 postFontSize ref 来实现这个效果:
1 | const posts = ref([ |
在模板中用它来控制所有博客文章的字体大小:
1 | <div :style="{ fontSize: postFontSize + 'em' }"> |
然后,给 <BlogPost> 组件添加一个按钮:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
父组件可以通过 v-on 或 @ 来自定义一个事件
1 | <BlogPost |
子组件可以通过调用内置的 $emit 方法,通过传入事件名称来抛出一个事件:
1 | <!-- BlogPost.vue, 省略了 <script> --> |
通过插槽来分配内容
一些情况下我们会希望能和 HTML 元素一样向组件中传递内容:
这可以通过 Vue 的自定义 <slot> 元素来实现:
我们使用 <slot> 作为一个占位符,父组件传递进来的内容就会渲染在这里
1 | <!-- AlertBox.vue --> |
动态组件
有些场景会需要在两个组件间来回切换,比如 Tab 界面:
上面的例子是通过 Vue 的 <component> 元素和特殊的 is attribute 实现的:
1 | <!-- currentTab 改变时组件也改变 --> |
在上面的例子中,被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用 is 属性 来创建一般的 HTML 元素。
当使用 <component :is="..."> 来在多个组件间作切换时,被切换掉的组件会被卸载。我们可以通过 `` 组件强制被切换掉的组件仍然保持“存活”的状态。
Vue路由—基础
Vue Router 是 Vue 官方的客户端路由解决方案。
客户端路由的作用是在单页应用 (SPA) 中将浏览器的 URL 和用户看到的内容绑定起来。当用户在应用中浏览不同页面时,URL 会随之更新,但页面不需要从服务器重新加载。
Vue Router 基于 Vue 的组件系统构建,你可以通过配置路由来告诉 Vue Router 为每个 URL 路径显示哪些组件。
创建路由器实例
路由器实例是通过调用 createRouter() 函数创建的:
1 | import { createRouter,createMemoryHistory } from 'vue-router' |
这里的 routes 选项定义了一组路由,把 URL 路径映射到组件。其中,由 component 参数指定的组件就是先前在 App.vue 中被 <RouterView> 渲染的组件。这些路由组件通常被称为视图,但本质上它们只是普通的 Vue 组件。
工作模式
这里的 history 选项控制了路由器的工作模式,下面详细解释一下路由器的工作模式:
注册路由器插件
一旦创建了我们的路由器实例,我们就需要将其注册为插件,这一步骤可以通过调用 use() 来完成。
1 | const app = createApp(App) |
和大多数的 Vue 插件一样,use() 需要在 mount() 之前调用。
如果你好奇这个插件做了什么,它的职责包括:
- 全局注册
RouterView和RouterLink组件。 - 添加全局
$router和$route属性。 - 启用
useRouter()和useRoute()组合式函数。 - 触发路由器解析初始路由。
访问路由器和当前路由
如果你是从 ES 模块导出路由器实例的,你可以将路由器实例直接导入到你需要它的地方。在一些情况下这是最好的方法。
在组件模板中,路由器实例将被暴露为 $router。这与同样被暴露的 $route 一样,但注意前者最后有一个额外的 r。
1 | <script setup> |
你现在不一定要完全理解这段代码,关键是要知道可以通过 useRouter() 和 useRoute() 来访问路由器实例和当前路由。
router 和 route
常常以 router 作为路由器实例提及。即由 createRouter() 返回的对象。在应用中,访问该对象的方式取决于上下文。在选项式 API 中,它可以通过 this.$router 来访问。
RouterView 和 RouterLink
在模板中,组件的名字可以是 PascalCase 风格或 kebab-case 风格的。Vue 的模板编译器支持两种格式,因此 <RouterView> 和 <router-view> 通常是等效的。此时应该遵循你自己项目中使用的约定。
如果使用 DOM 内模板,那么需要注意:组件名字必须使用 kebab-case 风格且不支持自闭合标签。因此你不能直接写 <RouterView />,而需要使用 <router-view></router-view>。
Vue路由—路由参数
query参数
两种传递方式:
1.直接拼接参数在路径中
1 | <routerLink to='/user/details?name=jack&age=20'></routerLink> |
2.写成对象形式
1 | <routerLink :to='{ |
params参数
当我们使用params参数时,有两个注意点:
1.定义路由规则时,需要指定占位参数
1 | const router = createRouter({ |
2.设置跳转时,只能用name,不能用path
1 | <routerLink :to='{ |
Vue路由—路由属性
props属性
props写法——布尔模式
下面这种实例中的写法,有这多层级的调用
1 | <!-- User.vue --> |
们可以通过 props 配置来解除这种行为:在路由规则当中,设置属性props为true时,如下
1 | const routes = [ |
此时,我们就可以在对应路由组件中定义defaultProps函数来接收该属性。因为props属性已经将params参数作为props传递给了User路由组件,所以此时就会变成下面这种写法:
1 | <script setup> |
props写法——函数写法
该写法可以自己决定将什么作为props传递给路由组件,注意return出去(query参数适用)
1 | const routes = [ |
1 | <script setup> |
props写法——对象写法
可以自己决定将什么作为props传递给路由组件。
1 | const routes = [ |
replace属性
在提及replace时,我们先说一个属性push,因为该属性是默认值。
1.属性位置:写在路由的routerLink标签中,因其是默认值所以可以不写
1 | <routerLink push></routerLink> |
2.功能:可以让我们返回上一个访问的页面。
3.工作方式:类似于栈的先进,就是最先访问的页面会被放在最里面。
4.图解:
replace属性,不是这样的,它不会让我们返回上一次访问的页面
1.属性位置:写在路由的routerLink标签中
1 | <routerLink replace></routerLink> |
2.功能:不能让我们返回上一个访问的页面。
3.工作方式:最先访问的页面会被后面访问的页面替换掉
4.图解:
Vue路由—编程式导航
除了使用 <router-link> 创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现。
1.拿到路由器对象实例
1 | impotr { useRouter } from 'vue-router' |
2.借助对象中的.push方法完成跳转
push会保留上一次的信息
1 | router.push(path:'/home',params:{......}) |
- 借助对象中的
.replace方法完成跳转
replace会替代掉上一次页面。
1 | router.replace(path:'/home',params:{......}) |
Vue路由—重定向
给路由添加redirect属性,下面例子是从 /home 重定向到 /:
1 | const routes = [{ path: '/home', redirect: '/' }] |
重定向的目标也可以是一个命名的路由:
1 | const routes = [{ path: '/home', redirect: { name: 'homepage' } }] |
甚至是一个方法,动态返回重定向目标:
1 | const routes = [ |
VueRouter路由守卫
在vue中,有三种类型的路由守卫:全局守卫、路由独享守卫、组件内守卫,下面分别来介绍
全局守卫
router.beforeEach:在任何路由跳转前触发
router.beforeResolve:在导航被确认前触发
router.afterEach:在导航完成后触发
1 | router.beforeEach((to, from, next) => { |
路由独享守卫
beforeEnter:直接在路由配置上定义,只在进入路由时触发
1 | const routes = [ |
组件内守卫
beforeRouteEnter:在渲染组件的对应路由被验证前调用
beforeRouteUpdate:在当前路由改变,但组件被复用时调用
beforeRouteLeave:在导航离开组件的对应路由时调用
Vue路由—404页面配置
- 不同的路由版本,配置404页面方式不同
Vr(VueRouter)3.x版本中:适用于Vue2
1 | //router.js中 |
Vr(VueRouter)4.x版本中:适用于Vue3
1 | // Vue Router 4.x (Vue 3) |
通用路由匹配参数不一致,需要注意
插槽
默认插槽
通过slot标签,来对要渲染的内容进行占位
把内容写在双标签内,通过slot对组件标签里的内容要进行占位,那么只要在双标签内写内容,该内容就会渲染到slot指定的位置
具名插槽
就是带有name标签属性的插槽,作用还是用来占位置,只是在向插槽添加内容时,有不同与默认插槽的写法
语法:
1 | <slot name='s1'></slot> |
写法:
使用template标签,并使用v-slot:xxx来指定插槽名称,xxx指的是名称,当然了我们也可以简写v-slot:xxx为#xxx
作用域插槽
适用场景:数据在子组件中,但根据数据生成的结构,由父组件决定
为何称为”作用域”?,因为数据是维护在子组件中的,父组件想要使用数据,那么这时候就产生了作用域问题,因为父组件访问不到子组件的数据
语法:
在插槽上通过props来传递数据即可,子组件中有如下写法:
1 | <template> |
在父组件中,不需要使用宏函数defineProps来进行接收,只需要在使用的模板上指定v-slot='xxx',这样,子组件上绑定的名为game数据就以一个对象的形式传到了父组件中
查看参数:
这时,通过params.game就能够访问到子组件中的数据,这就是作用域插槽的使用。
其他API:
shallowRef()
ref() 的浅层作用形式。
和 ref() 不同,浅层 ref 只会让第一层级的数据是响应式的,只有对 .value 的访问是响应式的。
示例
1 | const state = shallowRef({ count: 1 }) |
作用:shallowRef() 常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。
shallowReactive()
reactive() 的浅层作用形式。
和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的属性是响应式的。属性的值会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
1 | const state = shallowReactive({ |
readonly()
接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
只读代理是深层的:对任何嵌套属性的访问都将是只读的。它的 ref 解包行为与 reactive() 相同,但解包得到的值是只读的。
1 | const original = reactive({ count: 0 }) |
例子很明显,如果直接更改原属性,那么只读属性的copy也会跟着变化并触发侦听器,当我们直接更改只读属性时,这时由于属性是只读的,那么我们就会得到一个警告:该属性是只读属性!
shallowReadonly()
readonly() 的浅层作用形式
和 readonly() 不同,这里没有深层级的转换:只有根层级的属性变为了只读。属性的值都会被原样存储和暴露,这也意味着值为 ref 的属性不会被自动解包了。
1 | const state = shallowReadonly({ |
toRaw()
根据一个 Vue 创建的代理返回其原始对象。
详细信息
toRaw() 可以返回由 reactive()、readonly()、shallowReactive() 或者 shallowReadonly() 创建的代理对应的原始对象。
1 | <script setup> |
控制台输出:
markRaw()
将一个对象标记,并且该对象永远不能转为响应式对象。返回该对象本身。
1 | //markRaw() |
customRef()
customRef() 预期接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
示例
创建一个防抖 ref,即只在最近一次 set 调用后的1秒后再调用:
1 | <script setup> |
但是在真实的业务场景中,我们一般将其封装为一个hooks。
新建useMsg.js文件
代码如下:
1 | import { customRef } from 'vue'; |
使用:
1 | <script setup> |
查看控制台输出
内置组件
Teleport
<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
基本用法
应用场景
一个组件模板的一部分在逻辑上从属于该组件,但从整个应用视图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方。
这类场景最常见的例子就是全屏的模态框。(提示框)
1 | <template> |
1 | <script setup> |
当在初始 HTML 结构中使用这个组件时,会有一些潜在的问题:
position: fixed能够相对于浏览器窗口放置有一个条件,那就是不能有任何祖先元素设置了transform、perspective或者filter样式属性。也就是说如果我们想要用 CSStransform为祖先节点<div class="outer">设置动画,就会不小心破坏模态框的布局!- 这个模态框的
z-index受限于它的容器元素。如果有其他元素与<div class="outer">重叠并有更高的z-index,则它会覆盖住我们的模态框。
为了解决上述问题,Teleport组件提供了简单方法来实现。
1 | <button @click="open = true">Open Modal</button> |
这段代码中,使用了Teleport组件,to='body'用来指定一个传送的目标(意思是把Teleport内的代码渲染在什么地方),to的值可以是一个CSS选择器,也可以是一个DOM元素对象。上面的代码的作用就是告诉 Vue“把以下模板片段传送到 body 标签下”。
Vue中的事件总线
1 | //创建事件总线实例 |
如上述代码所示,我们通过new Vue()即可创建一个事件总线的对象。
事件总线符合:消息的订阅与发布,下面我们拿来详细介绍如何进行消息的订阅和发布。
消息订阅
订阅一个消息(事件)实际上就是监听他,对于事件总线对象(EventBus)来说,有如下用法
EventBus.$on()
- 参数1:事件名称
- 参数2:回调函数(接收参数)
注意事项:当你在销毁一个组件实例时记得要取消监听,避免内存泄漏。
取消监听:EventBus.$off()
- 参数:取消监听的事件名
消息发布
发布一个消息(事件)实际上就是触发这个事件,对于对象来说,有如下用法
EventBus.$emit()
- 参数1:触发的事件名称
- 参数2:要传递的数据
一次性订阅(事件只触发一次)
1 | eventBus.$once('login-success', (user) => { |
注意事项
- 避免内存泄漏:销毁组件前取消订阅
- 事件命名规范:使用命名空间比如
user:login,cart:update
在编程领域,命名空间(Namespace) 是一种组织标识符(如事件名、函数名、变量名)的方式,通过添加前缀或层级结构来避免命名冲突。在事件总线中使用命名空间,本质上是通过 “前缀 + 事件名” 的格式,让不同模块的事件拥有唯一标识。

































































