vue3学习-2(深入组件)
- 1.开始
- 2.基础
- 3.深入组件
- 注册
- 全局注册
- 局部注册
- 组件名格式
- Props
- Props 声明
- 响应式 Props 解构 3.5+
- 将解构的 props 传递到函数中
- 单向数据流
- 更改对象 / 数组类型的 props
- Prop 校验
- 事件
- 触发与监听事件
- 事件参数
- 声明触发的事件
- 事件校验
- 组件 v-model
- 基本用法
- 底层机制
- `v-model` 的参数
- 多个 `v-model` 绑定
- 处理 `v-model` 修饰符
- 透传 Attributes
- Attributes 继承
- 对 `class` 和 `style` 的合并
- `v-on` 监听器继承
- 深层组件继承
- 禁用 Attributes 继承
- 多根节点的 Attributes 继承
- 在 JavaScript 中访问透传 Attributes
- 插槽 Slots
- 插槽内容与出口
- 渲染作用域
- 默认内容
- 具名插槽
- 条件插槽
- 动态插槽名
- 作用域插槽
- 具名作用域插槽
- 高级列表组件示例
- 无渲染组件
- 依赖注入
- Prop 逐级透传问题
- Provide (提供)
- Inject (注入)
- 和响应式数据配合使用
- 使用 Symbol 作注入名
- 异步组件
- 基本用法
- 加载与错误状态
- 惰性激活 3.5
- 在空闲时进行激活
- 在可见时激活
- 在媒体查询匹配时进行激活
- 交互时激活
- 自定义策略
- 搭配 Suspense 使用
1.开始
2.基础
3.深入组件
注册
一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。
全局注册
Vue 应用实例的 .component()
方法,让组件在当前 Vue 应用中全局可用。
import { createApp } from 'vue'
const app = createApp({})
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
{
/* ... */
}
)
如果使用单文件组件,你可以注册被导入的 .vue
文件:
js
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
全局注册的组件可以在此应用的任意组件的模板中使用
局部注册
全局注册虽然很方便,但有以下几个问题:
- 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
- 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。
相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。
在使用 <script setup>
的单文件组件中,导入的组件可以直接在模板中使用,无需注册:
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
如果没有使用 <script setup>
,则需要使用 components
选项来显式注册:
import ComponentA from './ComponentA.js'
export default {
components: {
ComponentA
},
setup() {
// ...
}
}
对于每个 components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:
js
export default {
components: {
ComponentA: 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 声明
在使用 <script setup>
的单文件组件中,props 可以使用 defineProps()
宏来声明:
<script setup>
//字符串数组方式声明
const props = defineProps(['foo'])
//对象方式声明
//defineProps({
// title: String,
// likes: Number
//})
console.log(props.foo)
</script>
在没有使用 <script setup>
的组件中,props 可以使用 props
选项来声明:
export default {
//字符串数组方式声明
props: ['foo'],
//对象方式声明
//props: {
// title: String,
// likes: Number
//}
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
TypeScript 使用 <script setup>
,也可以使用类型标注来声明 props:
<script setup lang="ts">
defineProps<{
title?: string
likes?: number
}>()
</script>
响应式 Props 解构 3.5+
const { foo } = defineProps(['foo'])
watchEffect(() => {
// 在 3.5 之前只运行一次
// 在 3.5+ 中在 "foo" prop 变化时重新执行,`foo` 由编译器转换为 `props.foo`
// 在 3.4 及以下版本,`foo` 是一个实际的常量,永远不会改变。
console.log(foo)
})
此外,你可以使用 JavaScript 原生的默认值语法声明 props 默认值。这在使用基于类型的 props 声明时特别有用。
const { foo = 'hello' } = defineProps<{ foo?: string }>()
将解构的 props 传递到函数中
const { foo } = defineProps(['foo'])
// 错误,它等价于 watch(props.foo, ...)——我们给 watch 传递的是一个值而不是响应式数据源
// watch(foo, /* ... */)
watch(() => foo, /* ... */)
此外,当我们需要传递解构的 prop 到外部函数中并保持响应性时,这是推荐做法:
useComposable(() => foo)
传递组件的props官方推荐使用kebab-case,定义的时候推荐使用驼峰。
单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。
不应该在子组件中去更改一个 prop
-
prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可:
const props = defineProps(['initialCounter']) // 计数器只是将 props.initialCounter 作为初始值 // 像下面这样做就使 prop 和后续更新无关了 const counter = ref(props.initialCounter)
-
需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性:
const props = defineProps(['size']) // 该 prop 变更时计算属性也会自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())
更改对象 / 数组类型的 props
当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,不要改,否则子组件隐式传递给父组件。
Prop 校验
defineProps({
// 基础类型检查
// (给出 `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,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
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'
}
}
})
defineProps()
宏中的参数不可以访问 <script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
事件
触发与监听事件
在组件的模板表达式中,可以直接使用 $emit
方法触发自定义事件 (例如:在 v-on
的处理函数中):
<!-- MyComponent -->
<button @click="$emit('someEvent')">Click Me</button>
父组件可以通过 v-on
(缩写为 @
) 来监听事件:
<MyComponent @some-event="callback" />
同样,组件的事件监听器也支持 .once
修饰符:
<MyComponent @some-event.once="callback" />
建议:和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
事件参数
所有传入 $emit()
的额外参数都会被直接传向监听器。举例来说,$emit('foo', 1, 2, 3)
触发后,监听器函数将会收到这三个参数值。
声明触发的事件
组件可以显式地通过 defineEmits()
宏来声明它要触发的事件:
我们在 <template>
中使用的 $emit
方法不能在组件的 <script setup>
部分中使用,但 defineEmits()
会返回一个相同作用的函数供我们使用:
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
// 显式地使用了 setup 函数而不是 <script setup>
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}
也可以这样解构
export default {
emits: ['inFocus', 'submit'],
setup(props, { emit }) {
emit('submit')
}
}
事件校验
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
组件 v-model
基本用法
v-model
可以在组件上使用以实现双向绑定。
从 Vue 3.4 开始,推荐的实现方式是使用 defineModel()
宏:
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
父组件可以用 v-model
绑定一个值:
<!-- Parent.vue -->
<Child v-model="countModel" />
包装原生 input 元素
<script setup>
const model = defineModel()
</script>
<template>
<input v-model="model" />
</template>
底层机制
defineModel
是一个便利宏。编译器将其展开为以下内容:
- 一个名为
modelValue
的 prop,本地 ref 的值与其同步; - 一个名为
update:modelValue
的事件,当本地 ref 的值发生变更时触发。
在 3.4 版本之前,你一般会按照如下的方式来实现上述相同的子组件:
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
然后,父组件中的 v-model="foo"
将被编译为:
<!-- Parent.vue -->
<Child
:modelValue="foo"
@update:modelValue="$event => (foo = $event)"
/>
v-model
的参数
组件上的 v-model
也可以接受一个参数:
<MyComponent v-model:title="bookTitle" />
在子组件中,我们可以通过将字符串作为第一个参数传递给 defineModel()
来支持相应的参数:
<!-- MyComponent.vue -->
<script setup>
const title = defineModel('title')
</script>
<template>
<input type="text" v-model="title" />
</template>
如果需要额外的 prop 选项,应该在 model 名称之后传递:
const title = defineModel('title', { required: true })
多个 v-model
绑定
组件上的每一个 v-model
都会同步不同的 prop,而无需额外的选项:
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
处理 v-model
修饰符
创建一个自定义的修饰符 capitalize
,它会自动将 v-model
绑定输入的字符串值第一个字母转为大写:
<MyComponent v-model.capitalize="myText" />
通过像这样解构 defineModel()
的返回值,可以在子组件中访问添加到组件 v-model
的修饰符,可以给 defineModel()
传入 get
和 set
这两个选项,处理返回的值:
<script setup>
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
return value
}
})
</script>
<template>
<input type="text" v-model="model" />
</template>
透传 Attributes
Attributes 继承
- “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者
v-on
事件监听器。最常见的例子就是class
、style
和id
。
对 class
和 style
的合并
- 如果一个子组件的根元素已经有了
class
或style
attribute,它会和从父组件上继承的值合并。
v-on
监听器继承
- 子组件和父组件继承的监听器都会被触发
深层组件继承
例如子组件中有子子组件
-
此时
<MyButton>
接收的透传 attribute 会直接继续传给<BaseButton>
。请注意:
- 透传的 attribute 不会包含
<MyButton>
上声明过的 props 或是针对emits
声明事件的v-on
侦听函数,换句话说,声明过的 props 和侦听函数被<MyButton>
“消费”了。 - 透传的 attribute 若符合声明,也可以作为 props 传入
<BaseButton>
。
- 透传的 attribute 不会包含
禁用 Attributes 继承
-
不想要一个组件自动地继承 attribute,从 3.3 开始你也可以直接在
<script setup>
中使用defineOptions
:<script setup> defineOptions({ inheritAttrs: false }) // ...setup 逻辑 </script>
-
这些透传进来的 attribute 可以在模板的表达式中直接用
$attrs
访问到。$attrs
对象包含了除组件所声明的props
和emits
之外的所有其他 attribute,例如class
,style
,v-on
监听器等等。 -
和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像
foo-bar
这样的一个 attribute 需要通过$attrs['foo-bar']
来访问。 -
像
@click
这样的一个v-on
事件监听器将在此对象下被暴露为一个函数$attrs.onClick
。如果只是想在button应用透传来的attributes 在div不使用
<div class="btn-wrapper"> <button class="btn" v-bind="$attrs">Click Me</button> </div>
没有参数的
v-bind
会将一个对象的所有属性都作为 attribute 应用到目标元素上。
多根节点的 Attributes 继承
-
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果
$attrs
没有被显式绑定,将会抛出一个运行时警告。<CustomLayout id="custom-layout" @click="changeValue" />
如果
$attrs
被显式绑定,则不会有警告:<header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer>
在 JavaScript 中访问透传 Attributes
你可以在 <script setup>
中使用 useAttrs()
API 来访问一个组件的所有透传 attribute:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
如果没有使用 <script setup>
,attrs
会作为 setup()
上下文对象的一个属性暴露:
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
attrs
的特性:attrs
总是反映最新的透传属性,但不是响应式的。- 不能通过
watch
或computed
监听其变化。
- 解决方案:
- 使用
onUpdated
生命周期钩子,在每次更新时访问最新的attrs
。 - 使用
watchEffect
或watch
,尽管attrs
本身非响应式,但可以通过这些工具响应某些属性的变化。 - 如果需要响应性,可以将属性定义为
props
。
- 使用
插槽 Slots
插槽内容与出口
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件
渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。插槽内容无法访问子组件的数据。
默认内容
中添加内容后,如果父组件不传内容,会显示这个默认内容
具名插槽
<script setup>
import BaseLayout from './BaseLayout.vue'
</script>
<template>
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<!-- 默认的用#default或者不用 #和v-slot一样 -->
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
</template>
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<style>
footer {
border-top: 1px solid #ccc;
color: #666;
font-size: 0.8em;
}
</style>
条件插槽
根据传入的插槽名包装样式
<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>
动态插槽名
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
作用域插槽
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
你可以将作用域插槽类比为一个传入子组件的函数。子组件会将相应的 props 作为参数传给它。
具名作用域插槽
v-slot:name="slotProps"
。当使用缩写时是这样
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
<slot name="header" message="hello"></slot>
注意插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps
的结果是 { message: 'hello' }
。
如果你同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。尝试直接为组件添加 v-slot
指令将导致编译错误。
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
高级列表组件示例
看一个 <FancyList>
组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。我们期望的用法可能是这样的:
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
在 <FancyList>
之中,我们可以多次渲染 <slot>
并每次都提供不同的数据 (注意我们这里使用了 v-bind
来传递插槽的 props):
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
无渲染组件
一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件。
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销。
依赖注入
Prop 逐级透传问题
有一些多层级嵌套的组件,深层的组件需要用一个较远的祖先组件的时候,需要层层传递,中间组件是不需要这些props的但是需要层层传递,这一问题被称为“prop 逐级透传”,这样一是会影响其他组件,二是比较麻烦,这里就会使用到provide
和 inject
。一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
Provide (提供)
要为组件后代提供数据,需要使用到 provide()
函数:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
如果不使用 <script setup>
,请确保 provide()
是在 setup()
同步调用的:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
provide()
函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol
。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
也可以在应用层全局提供。
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
Inject (注入)
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
//默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值
const value = inject('key', () => new ExpensiveClass(), true)
和响应式数据配合使用
当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用 readonly()
来包装提供的值
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
使用 Symbol 作注入名
正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, {
/* 要提供的数据 */
})
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
异步组件
基本用法
在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件。Vue 提供了 defineAsyncComponent
方法来实现此功能:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
加载与错误状态
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
惰性激活 3.5
在空闲时进行激活
通过 requestIdleCallback
进行激活:
import { defineAsyncComponent, hydrateOnIdle } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnIdle(/* 传递可选的最大超时 */)
})
在可见时激活
通过 IntersectionObserver
在元素变为可见时进行激活。
import { defineAsyncComponent, hydrateOnVisible } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnVisible()
})
可以选择传递一个侦听器的选项对象值:
hydrateOnVisible({ rootMargin: '100px' })
在媒体查询匹配时进行激活
当指定的媒体查询匹配时进行激活。
import { defineAsyncComponent, hydrateOnMediaQuery } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnMediaQuery('(max-width:500px)')
})
交互时激活
当组件元素上触发指定事件时进行激活。完成激活后,触发激活的事件也将被重放。
import { defineAsyncComponent, hydrateOnInteraction } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: hydrateOnInteraction('click')
})
也可以是多个事件类型的列表:
hydrateOnInteraction(['wheel', 'mouseover'])
自定义策略
import { defineAsyncComponent, type HydrationStrategy } from 'vue'
const myStrategy: HydrationStrategy = (hydrate, forEachElement) => {
// forEachElement 是一个遍历组件未激活的 DOM 中所有根元素的辅助函数,
// 因为根元素可能是一个片段而非单个元素
forEachElement(el => {
// ...
})
// 准备好时调用 `hydrate`
hydrate()
return () => {
// 如必要,返回一个销毁函数
}
}
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
hydrate: myStrategy
})
搭配 Suspense 使用
异步组件可以搭配内置的 <Suspense>
组件一起使用,若想了解 <Suspense>
和异步组件之间交互,请参阅 <Suspense>
章节。