前言
本文为本人在学习vue时所记录的有关组件的笔记,其中详细介绍了vue中的组件及其使用,其中大部分内容来自vue官方中文文档,对于其中的一些知识也加入了一些自己的理解。
文章目录
基础
什么是组件?
组件允许我们将 UI 划分为独立的、可重用的部分,就像我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。或者说每一个组件都像是一个函数一样,是独立的,可以进行复用。
我们一般会将 Vue 组件定义在一个单独的 vue文件中,这被叫做单文件组件(简称 SFC),例如:
<script> export default { data() { return { count: 0 } } } </script> <template> <button @click="count++">You clicked me {{ count }} times.</button> </template>
要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。例如:
<script> import ButtonCounter from './ButtonCounter.vue' export default { components: { ButtonCounter } } </script> <template> <h1>Here is a child component!</h1> <ButtonCounter /> </template>
组件可以被重用任意多次,并且每个组件之中的内容都是独立,不会被影响。例如:
<h1>Here is a child component!</h1> <ButtonCounter /> <ButtonCounter /> <ButtonCounter />
注册
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
全局注册
我们可以使用 Vue 应用实例的 .component() 方法,让组件在当前 Vue 应用中全局可用。例如:
import { createApp } from 'vue' const app = createApp({}) app.component( // 注册的名字 'MyComponent', // 组件的实现 { /* ... */ } )
如果使用单文件组件,你可以注册被导入的 .vue 文件:
import MyComponent from './App.vue' app.component('MyComponent', MyComponent)
.component() 方法可以被链式调用:
app .component('ComponentA', ComponentA) .component('ComponentB', ComponentB) .component('ComponentC', ComponentC)
全局注册的组件可以在此应用的任意组件的模板中使用:
<!-- 这在当前应用的任意组件中都可用 --> <ComponentA/> <ComponentB/> <ComponentC/>
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
局部注册需要使用 components 选项:
<script> import ComponentA from './ComponentA.vue' export default { components: { ComponentA } } </script> <template> <ComponentA /> </template>
对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。
props
声明
首先什么是props?
Props 是一种特别的 attributes,我个人理解props就是一种用于从父组件向子组件传递数据的机制。具体来说,props 是子组件接收父组件传递的数据的一种方式。子组件通过 props 属性来声明自己接收哪些数据,并可以在其模板中使用这些数据。
props在使用时需要先被显式声明,这样vue才知道你用了哪些props。
首先定义props可以用props选项来定义,例如我想传给该组件一个info信息,那我就必须在该组件内声明一下:
export default { props: ['info'], created() { console.log(this.info) } }
除此之外,还可以使用对象式声明的方式,例如,假设我们有一个博文组件 BlogPost.vue,父组件想要向这个博文组件传递标题、内容和作者信息。我们可以这样声明 props:
<!-- BlogPost.vue --> <template> <div class="blog-post"> <h2>{{ title }}</h2> <p>{{ content }}</p> <p>Written by: {{ author }}</p> </div> </template> <script> export default { props: { title: { type: String, required: true }, content: { type: String, default: 'No content available' }, author: { type: String, default: 'Anonymous' } } } </script>
在这个例子中:
- title 属性必须是一个字符串,并且是必需的。如果父组件未传递 title,Vue.js 将会发出警告。
- content 属性是一个可选的字符串,默认值为 No content available。如果父组件未传递 content,则显示默认内容。
- author 属性是一个可选的字符串,默认值为 Anonymous。如果父组件未传递 author,则显示作者为匿名。
父组件可以这样使用 BlogPost组件:
<!-- ParentComponent.vue --> <template> <div> <BlogPost title="Vue.js Props Example" content="This is an example of using props in Vue.js." author="John Doe" /> </div> </template> <script> import BlogPost from './BlogPost.vue'; export default { components: { BlogPost } } </script>
上面的例子中props的值都是静态的,实际上也可以是动态的,可以使用v-bind或:来进行动态绑定,其次他还可以是多种数据类型(String,Number,Boolean,Array,Object),例如:
<BlogPost :title="post.title" /> <BlogPost :title="post.title + ' by ' + post.author.name" /> <BlogPost :likes="42" /> <BlogPost :is-published="false" /> <BlogPost :comment-ids="[234, 266, 273]" /> <BlogPost :author="{ name: 'Veronica', company: 'Veridian Dynamics' }" /> <!-- 根据一个变量的值动态传入 --> <BlogPost :author="post.author" />
一个对象内还可以绑定多个prop,例如
export default { data() { return { post: { id: 1, title: 'My Journey with Vue' } } } }
<BlogPost v-bind="post" /> <!-- 等价于 --> <BlogPost :id="post.id" :title="post.title" />
注意:所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。若你这么做了,Vue 会在控制台上向你抛出警告:例如:
export default { props: ['foo'], created() { // ❌ 警告!prop 是只读的! this.foo = 'bar' } }
也即是说爸爸让你改你才能改,子组件不能擅作主张自己更改自己的props。
校验
Vue 组件可以更细致地声明对传入的 props 的校验要求。比如我们上面已经看到过的类型声明,如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。要声明对 props 的校验,你可以向 props 选项提供一个带有 props 校验选项的对象,校验选项中的 type 可以是下列这些原生构造函数:String、Number、Boolean、Array、Object、Date、Function、Symbol、Error。例如:
export default { props: { // 基础类型检查 //(给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: { type: String, required: true }, // 必传但可为 null 的字符串 propD: { type: [String, null], required: true }, // Number 类型的默认值 propE: { type: Number, default: 100 }, // 对象类型的默认值 propF: { type: Object, // 对象或者数组应当用工厂函数返回。 // 工厂函数会收到组件所接收的原始 props // 作为参数 default(rawProps) { return { message: 'hello' } } }, // 自定义类型校验函数 // 在 3.4+ 中完整的 props 作为第二个参数传入 propG: { validator(value, props) { // The value must match one of these strings return ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propH: { type: Function, // 不像对象或数组的默认,这不是一个 // 工厂函数。这会是一个用来作为默认值的函数 default() { return 'Default function' } } } }
另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如下面这个类:
class Person { constructor(firstName, lastName) { this.firstName = firstName this.lastName = lastName } }
export default { props: { author: Person } }
一些补充细节:
- 所有 prop 默认都是可选的,除非声明了 required: true。
- 除 Boolean外的未传递的可选 prop 将会有一个默认值 undefined。
- Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
- 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。
当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。
校验选项中的 type 可以是下列这些原生构造函数:String、Number、Boolean、Array、Object、Date、Function、Symbol、Error
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 组件为例
export default { props: { disabled: Boolean } }
该组件可以被这样使用:
<!-- 等同于传入 :disabled="true" --> <MyComponent disabled /> <!-- 等同于传入 :disabled="false" --> <MyComponent />
当一个 prop 被声明为允许多种类型时Boolean 的转换规则也将被应用。然而,当同时允许 String 和 Boolean 时,有一种边缘情况——只有当 Boolean出现在 String 之前时,Boolean 转换规则才适用:
// disabled 将被转换为 true export default { props: { disabled: [Boolean, Number] } } // disabled 将被转换为 true export default { props: { disabled: [Boolean, String] } } // disabled 将被转换为 true export default { props: { disabled: [Number, Boolean] } } // disabled 将被解析为空字符串 (disabled="") export default { props: { disabled: [String, Boolean] } }
事件
可以使用$emit方法触发自定义事件,例如在一个按钮中
<button @click="$emit('要触发的事件')">Click Me</button>
e m i t ( ) 方法在组件实例上也同样以 t h i s . emit() 方法在组件实例上也同样以 this. emit()方法在组件实例上也同样以this.emit() 的形式可用:
export default { methods: { submit() { this.$emit('someEvent') } } }
父组件可以通过 v-on (缩写为 @) 来监听事件:
同样,组件的事件监听器也支持 .once 修饰符,也就是修饰了只会触发一次:
<MyComponent @some-event="callback" /> <MyComponent @some-event.once="callback" />
事件参数
有时事件在触发时需要携带一些参数,举例来说,我们想要 组件来管理文本会缩放得多大。我们可以在按钮上绑定一个“increaseBy”事件,并传递一个额外的参数1。
<button @click="$emit('increaseBy', 1)">Increase by 1</button>
然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:
<MyButton @increase-by="(n) => count += n" />
这样在每次点击按钮后,count的值就会加一。
或者,也可以用一个组件方法来作为事件处理函数:
<MyButton @increase-by="increaseCount" />
再写个方法来接收传递的参数
methods: { increaseCount(n) { this.count += n } }
v-model
v-model 可以在组件上使用以实现双向绑定
例如现有组件,其界面为让输入框内的文字和参数modelValue保持一致,输入框内更新,参数值也会更新。
<script> export default { props: ['modelValue'], emits: ['update:modelValue'] } </script> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>
那么在使用该组件时就可以用v-model来和组件中的值进行绑定,例如:
<script> import CustomInput from './CustomInput.vue' export default { components: { CustomInput }, data() { return { message: '初始值' } } } </script> <template> <CustomInput v-model="message" /> {{ message }} </template>
这样message的值就和输入框中的值就行了绑定,最后呈现的效果为在输入框后面的文字回合输入框中保持一致。
此外,v-model还可以接收一个参数。例如现在有一个标题参数title
<MyComponent v-model:title="bookTitle" />
子组件中使用 title prop 和 update:title 事件来更新父组件的值,而非默认的 modelValue prop 和 update:modelValue事件:。例如:
<script> export default { props: ['title'], emits: ['update:title'] } </script> <template> <input type="text" :value="title" @input="$emit('update:title', $event.target.value)" /> </template>
上面的都是一个v-model的,一个组件上还可以绑定多个有参数的v-model
例如:
<UserName v-model:first-name="first" v-model:last-name="last"/>
<script> export default { props: { firstName: String, lastName: String }, emits: ['update:firstName', 'update:lastName'] } </script> <template> <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)" /> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)" /> </template>
v-model 有一些内置的修饰符,例如 .trim,.number 和 .lazy。在组件中还可以自定义一些修饰符。
例如:创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:
父组件中代码如下
<MyComponent v-model.capitalize="myText" />
子组件:每次输入更新时都会触发emitValue方法,方法中会将第一个单词变为大写
<script> export default { props: { modelValue: String, modelModifiers: { default: () => ({}) } }, emits: ['update:modelValue'], methods: { emitValue(e) { let value = e.target.value if (this.modelModifiers.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) } this.$emit('update:modelValue', value) } } } </script> <template> <input type="text" :value="modelValue" @input="emitValue" /> </template>
另外,在使用修饰符的同时加上参数也是可以的
例如:
<UserName v-model:first-name.capitalize="first" v-model:last-name.uppercase="last" />
<script> export default { props: { firstName: String, lastName: String, firstNameModifiers: { default: () => ({}) }, lastNameModifiers: { default: () => ({}) } }, emits: ['update:firstName', 'update:lastName'], created() { console.log(this.firstNameModifiers) // { capitalize: true } console.log(this.lastNameModifiers) // { uppercase: true } } } </script>
透传Attributes
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或emits的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和id 。
class继承
例如现有模板MyButton中一个按钮:
<button>我是按钮</button>
在父组件中使用时加上了class样式,如:
<MyButton class="large" />
最后渲染出的结果就是
<button class="large">我是按钮</button>
父组件中的样式将会传递给模板中按钮,倘若子组件中已有class,则会把样式进行合并。
例如子组件中有按钮:
<button class="small">我是按钮</button>
最后的结果就会变为
<button class="small large">我是按钮</button>
v-on继承
这样的规则也适用于v-on事件监听器中,如果父组件中有个点击事件,那么模板中的元素被点击时也会触发父组件的事件,同样,如果子组件中已有事件,那么点击后子组件的事件和父组件的事件都会被触发。
深层组件继承
如果子组件中又使用了另一个子组件,例如组件中根节点,那么在使用
时传递的attribute会直接传递给
禁用继承
这种继承关系有时可能我们不想让它生效,此时可以自己在组件选项中设置inheritAttrs:false
例如:
<template> <div> <!-- 组件内容 --> </div> </template> <script> export default { inheritAttrs: false, // 禁止继承父组件的属性到根元素 // 其他组件选项 }; </script>
多根节点的Attribute继承
如果一个组件中有多个根节点,vue并不会自动透传所有的根节点,此时需要使用$attrs进行显示绑定,否则vue不知道要把attribute透传到哪里。例如在组件中,这样父组件中的attribute就会精准透传到中
<name v-bind:"$attrs">...</name> <age>..</age> <weight>...</weight>
js中访问attribute
在js中可以在
例如:
<script setup> import { useAttrs } from 'vue' const attrs userAttrs() </script>
插槽Slots
插槽内容
插槽可以让接收模板内容,为子组件传递一些模块片段,并让子组件渲染这些片段。
例如当前有一个组件如下:
<template> <button class="fancy-btn"> <slot/> </button> </template> <style> .fancy-btn { color: #fff; background: linear-gradient(315deg, #42d392 25%, #647eff); border: none; padding: 5px 10px; margin: 5px; border-radius: 8px; cursor: pointer; } </style>
在使用时如下:
<template> <FancyButton> 请点击我 <!-- slot content --> </FancyButton> </template>
最终渲染如下
使用时中的“请点击我”就会替换掉,实现如上效果,插槽中的内容可以是任意合法的模板内容,并且插槽内容可以访问到父组件的数据作用域,因此可以传入多个元素或者是组件,例如有一个点击后数值加一的按钮,还有个组件,内容很简单只有一个❤如下:
<template>❤️</template>
在使用时就可以把它也传入:
<script> import FancyButton from './FancyButton.vue' import AwesomeIcon from './AwesomeIcon.vue' export default { components: { FancyButton, AwesomeIcon }, data() { return { count: 1 } } } </script> <template> <span >{{ count }}</span> <FancyButton @click="count++">点一下加一,当前为:{{ count }}</FancyButton> <FancyButton> <span style="color:cyan">点我! </span> <AwesomeIcon /> </FancyButton> </template>
最终效果如下:
在外部没有提供任何内容的情况下,可以为插槽指定默认内容,比如有这样一个 组件:
<template> <button type="submit"> <slot> 默认内容 </slot> </button> </template>
在使用时如果不传入内容就会默认展示默认内容,有新内容则展示新内容
最终效果如下:
### 具名插槽
一个组件中还可以包含多个插槽出口,每一个元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:
例如现有组件如下:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>
这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 出口会隐式地命名为“default”。
在父组件中使用该组件时,可以将多个插槽内容传入到各自目标插槽的出口。具体做法为,我们需要使用一个含 v-slot (可以简写为#)指令的 元素,并将目标插槽的名字传给该指令:
例如:
<BaseLayout> <template #header> <h1>Here might be a page title</h1> </template> <!-- 隐式的默认插槽 --> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </BaseLayout>
这样就做到了具体的替换。
最终渲染效果如下:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer> </div>
条件插槽
有时需要根据插槽是否存在来渲染某些内容,这时可以结合使用$slot属性与v-if来实现。
在下面的示例中,我们定义了一个卡片组件,它拥有三个条件插槽:header、footer 和 default。 当 header、footer 或 default 存在时,我们希望包装它们以提供额外的样式,也就是会将div中符合v-if的提供额外的样式。
<template> <div class="card"> <div v-if="$slots.header" class="card-header"> <slot name="header" /> </div> <div v-if="$slots.default" class="card-content"> <slot /> </div> <div v-if="$slots.footer" class="card-footer"> <slot name="footer" /> </div> </div> </template>
动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template> <!-- 缩写为 --> <template #[dynamicSlotName]> ... </template> </base-layout>
作用域插槽
如果想要同时使用父组件域内和子组件域内的数据。要做到这一点,我们可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes:
<div> <slot :text="欢迎光临" :count="1"></slot> </div>
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。下面通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props 对象:
<MyComponent v-slot="slotProps"> {{ slotProps.text }} {{ slotProps.count }} </MyComponent>
这样在子组件域内就拿到了父组件中的text和count
具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot
指令的值被访问到:v-slot:name=“slotProps”。当使用缩写时是这样:
<MyComponent> <template #header="headerProps"> {{ headerProps }} </template> </MyComponent>
向具名插槽中传入 props:
<slot name="header" message="hello"></slot>
这里要特别注意一点:
插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: ‘hello’ }。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 标签。尝试直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。举例:
<div> <slot :message="hello"></slot> <slot name="footer" /> </div>
<!-- 该模板无法编译 --> <MyComponent v-slot="{ message }"> <p>{{ message }}</p> <template #footer> <!-- message 属于默认插槽,此处不可用 --> <p>{{ message }}</p> </template> </MyComponent>
为默认插槽使用显式的 <template>
标签有助于更清晰地指出 message
属性在其他插槽中不可用:
<MyComponent> <!-- 使用显式的默认插槽 --> <template #default="{ message }"> <p>{{ message }}</p> </template> <template #footer> <p>Here's some contact info</p> </template> </MyComponent>
高级列表组件示例
下面展示一个作用域插槽的应用场景,来看一个 `组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。
<template> <FancyList api-url="url" :per-page="10"> <template #item="{ body, username, likes }"> <div class="item"> <p>{{ body }}</p> <p class="meta">by {{ username }} | {{ likes }} likes</p> </div> </template> </FancyList> </template>
<script> export default { props: ['api-url', 'per-page'], data() { return { items: [] } }, mounted() { setTimeout(() => { this.items = [ { body: 'Scoped Slots Guide', username: 'Evan You', likes: 20 }, { body: 'Vue Tutorial', username: 'Natalia Tepluhina', likes: 10 } ] }, 1000) } } </script> <template> <ul> <li v-if="!items.length"> Loading... </li> <li v-for="item in items"> <slot name="item" v-bind="item"/> </li> </ul> </template>
最终效果如下:
依赖注入
通常情况下,当我们需要从父组件向子组件传递数据时,会使用 props。想象一下这样的结构:有一些多层级嵌套的组件,形成了一棵巨大的组件树,而某个深层的子组件需要一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链逐级传递下去,这会非常麻烦。
provide 和inject 可以帮助我们解决这一问题[1]。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入中父组件提供给整条链路的依赖。
provide
可以使用provide()为组件后代提供数据,例如
<script setup> import { provide } from 'vue' provide('注入名',值) <script>
或
import { provide } from 'vue' export default { setup() { provide('注入名',值) } }
provide()会接收两个参数,第一个是注入名,可以是字符串或是一个Symbol。第二个参数就是要提供的值,可以是任意类型的数据。
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖,例如
import { createApp } from 'vue' const app = createApp({}) app.provide('message',1)
在应用级别提供的数据在该应用内的所有组件中都可以注入。
Inject
要注入上层组件提供的数据,需使用inject()函数,例如:
<script setup> import {inject } from 'vue' const message = inject('message') </script>
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass(), true)
第三个参数表示默认值应该被当作一个工厂函数。
异步组件
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了defineAsynccomponent 方法来实现此功能。例如:
import { defineAsyncComponent } from 'vue' const AsyncComp = defineAsyncComponent(() => { return new Promise((resolve, reject) => { // ...从服务器获取组件 resolve(/* 获取到的组件 */) }) })
defineAsynccomponent方法接收一个返回 Promise 的加载函数。这个 Promise的 resolve 回调方法应该在从服务器获得组件定义时调用。也可以调用 reject(reason)表明加载失败。
最后得到的 AsyncComp 是一个外层包装过的组件,仅在负面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
与普通组件一样,异步组件可以使用app.component全局注册,例如
app.component('MyComponent', defineAsyncComponent(() => import('./components/MyComponent.vue') ))
或者在父组件中直接定义
<script setup> import { defineAsyncComponent } from 'vue' const AdminPage = defineAsyncComponent(() =>import('./components/AdminPageComponent.vue') ) </script> <template> <AdminPage /> </template>
加载和错误
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsynccomponent()也支持在高级选项中处理这些状态,例如:
const AsyncComp = defineAsyncComponent({ // 加载函数 loader: () => import('./Foo.vue'), // 加载异步组件时使用的组件 loadingComponent: LoadingComponent, // 展示加载组件前的延迟时间,默认为 200ms delay: 200, // 加载失败后展示的组件 errorComponent: ErrorComponent, // 如果提供了一个 timeout 时间限制,并超时了 // 也会显示这里配置的报错组件,默认值是:Infinity timeout: 3000 })
内置组件
component
是一个抽象的组件,用于动态地渲染不同的组件或元素。
通过绑定 is 属性可以实现动态组件的切换和渲染。
<component :is="currentComponent"></component>
transition
和 提供了在 Vue.js 中实现过渡和动画效果的功能。
通过定义过渡的 CSS 类名,可以控制元素在进入或离开 DOM 时的动画效果。
当一个 组件中的元素被插入或移除时,会发生下面这些事情:
- Vue 会自动检测目标元素是否应用了 CSS 过渡或动画。如果是,则一些 CSS 过渡 class会在适当的时机被添加和移除。
- 如果有作为监听器的 JavaScript 钩子,这些钩子函数会在适当时机被调用。
- 如果没有探测到 CSS 过渡或动画、也没有提供 JavaScript 钩子,那么 DOM 的插入、删除操作将在浏览器的下一个动画帧后执行。
使用例子:最后的效果为点击一次按钮后,下面的文字会出现或者消失。实现的方法为定义好元素进入时和消失时的状态,在写好过渡时的效果。
<script setup> import { ref } from 'vue' const show = ref(true) </script> <template> <button @click="show = !show">按钮</button> <Transition> <p v-if="show">我的名字</p> </Transition> </template> <style> .v-enter-active, .v-leave-active { transition: opacity 0.5s ease; } .v-enter-from, .v-leave-to { opacity: 0; } </style>
过程如下:
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
- v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是
v-enter-from
被移除的同时),在过渡或动画完成之后移除。 - v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
- v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
- v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是
v-leave-from
被移除的同时),在过渡或动画完成之后移除。
keep-alive
是一个抽象组件,用于保持组件状态或避免多次渲染。
当组件被 包裹时,其状态将会被缓存,而不是每次切换时重新渲染。
例如:下方引用了两个组件A和B
<script setup> import { shallowRef } from 'vue' import CompA from './CompA.vue' import CompB from './CompB.vue' const current = shallowRef(CompA) </script> <template> <div class="demo"> <label><input type="radio" v-model="current" :value="CompA" /> A</label> <label><input type="radio" v-model="current" :value="CompB" /> B</label> <KeepAlive> <component :is="current"></component> </KeepAlive> </div> </template>
页面中渲染效果如下:点击上面的选项会切换下面的内容,不过由于组件使用了KeepAlive,因此每次切换时不会重复渲染,会保留之前的值。
teleport
允许你将 DOM 元素渲染到应用的任何地方,而不受当前 DOM 结构的限制。这在需要在应用中动态移动元素时非常有用,例如在模态框中渲染弹出内容。
<button @click="open = true">Open Modal</button> <Teleport to="body"> <div v-if="open" class="modal"> <p>Hello from the modal!</p> <button @click="open = false">Close</button> </div> </Teleport>
Suspense
是 Vue.js 3.x 中新增的组件,用于处理异步组件的加载和状态。它可以在异步组件加载完成之前显示占位内容,并处理加载状态和错误。例如:
<Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <div>Loading...</div> </template> </Suspense>