插槽
插槽内容和出口
适用于为子组件传递一些模板片段。子组件定义插槽出口,父组件传递插槽内容。
举例来说,这里有一个 <FancyButton>
组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton>
的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }}
插值表达式渲染的内容都是一样的,来自FancyButton的父组件。插槽内容无法访问子组件的数据——除非使用作用域插槽。
默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。
<!-- <SubmitButton> -->
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
具名插槽
在一个 <BaseLayout>
组件中:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
这类带 name
的插槽被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default”。
要为具名插槽传入内容,我们需要使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot
有对应的简写 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。
<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>
当然,插槽名可以是动态的,<slot name="[slotName]"></slot>
的形式定义插槽出口。
作用域插槽
在渲染作用域中提到,插槽内容在父组件作用域中渲染,无法访问子组件的数据。要想让父组件在插槽内容可以使用子组件数据,可以像传prop一样,在子组件定义插槽出口时传attributes。
<slot text="hello" :count="count"></slot>
上面传了两个attributes,text是静态的,count是动态的,父组件可以介绍的这两个值。
匿名作用域插槽
子组件模板:
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
父组件模板:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
具名作用域插槽
子组件MyComponent:
<script>
export default {
data() {
return {
greetingMessage: 'hello'
}
}
}
</script>
<template>
<div>
<slot name="content" :text="greetingMessage" :count="1"></slot>
</div>
</template>
父组件App:
<script setup>
import MyComponent from './MyComponent.vue'
import { ref } from 'vue'
const msg = ref('具名作用域插槽')
</script>
<template>
<h1>{{ msg }}</h1>
<MyComponent v-slot:content="slotProps">{{slotProps}}</MyComponent>
<MyComponent v-slot:content="{text, count}">{{text}} -- {{ count}}</MyComponent>
<MyComponent>
<template #content="{text, count}">{{text}} -- {{ count}}</template>
</MyComponent>
</template>
注意:插槽上的 name
不会作为 props 传递给插槽。
混用匿名和具名作用域插槽
子组件BaseLayout:
<script>
export default {
data() {
return {
greetingMessage: 'hello'
}
}
}
</script>
<template>
<div class="container">
<header>
<slot name="header" :text="greetingMessage"></slot>
</header>
<main>
<slot :text="greetingMessage"></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
父组件App:
<script setup>
import MyComponent from './MyComponent.vue'
import { ref } from 'vue'
const msg = ref('混用匿名和具名作用域插槽')
const text = ref('parent')
</script>
<template>
<h1>{{ msg }}</h1>
<h2>{{text}}</h2>
<!-- 方式一 -->
<MyComponent>
<template #header="{text}">header: {{text}}</template>
<template #default="{text}">default: {{text}} —— 来自子组件</template>
<template #footer>footer: {{text}}</template>
</MyComponent>
<hr />
<!-- 方式二 -->
<MyComponent>
<template #header="{text}">header:{{text}}</template>
default: {{text}} —— 来自父组件
<template #footer>footer: {{text}}</template>
</MyComponent>
</template>
父子组件都有一个text,子组件将text通过插槽传递。可以看到,方式一的默认插槽接收了子组件的text,显示的是hello;而方式二的默认插槽没有接收子组件的text,显示的是parent。
需要注意的是,混用具名插槽与默认插槽时,不能在MyComponent上使用v-slot,可以将默认插槽放在template里面。
<!-- 该模板无法编译 -->
<template>
<MyComponent v-slot="{ text }">
<p>{{ message }}</p>
<template #header>
<p>{{ text }}</p>
</template>
</MyComponent>
</template>
依赖注入
Prop逐级透传
很深的子组件DeepChild需要接收祖先组件Root的prop,从Root到DeepChild链路上的节点需要接收和向下传递prop,虽然链路上的这些节点并不需要这个prop。这一问题被称为“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
。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide()
,使用不同的注入名,注入不同的依赖值。
第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref。
还可以在整个应用层面提供依赖:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
Inject
要注入上层组件提供的数据,需使用 inject 函数:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
如果没有使用 <script setup>
,inject()
需要在 setup()
内同步调用:
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
注入默认值
inject
传入的注入名由某个祖先链上的组件提供。如果该注入名的确没有任何组件提供,则会抛出一个运行时警告。如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值。
// 如果没有祖先组件提供 "message"
// `value` 会是 "这是默认值"
const value = inject('message', '这是默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
const value = inject('key', () => new ExpensiveClass())
和响应式数据配合使用
当提供 / 注入响应式的数据时, 尽可能将任何对响应式状态的变更都保持在供给方组件中。如果需要注入方组件需要更改注入的数据,那么将更改数据的方法也在供给方组件内提供。
<!-- 在供给方组件内 -->
<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)
异步组件
基本用法
使用defineAsyncComponent异步加载组件,该方法接收一个返回 Promise 的加载函数。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent
搭配使用:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
最后得到的 AsyncComp
是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。异步组件的注册和普通组件一样,全局注册或局部注册,参考以前章节。
加载和错误状态
异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent()
也支持在高级选项中处理这些状态:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。