一.穿透Attributes
- Attributes继承
“透传attribute”指的是传递给一个组件,却没有被该组件声明为props或emits的attribute或者v-on事件监听器。最常见例子class,style和id.
当一个组件以单个元素为根作渲染时,透传的attribute会自动被添加到根元素上。
//假如有一个<MyButton>,它的模板长这样:
<MyButton>
<button> click me <button/>
//一个父组件使用了这个组件,并传入class
<MyButton class="large">
//最后渲染出来的DOM是:
<button class="large">click me </button>
//这里,<MyButton>并没有将class声明为一个它所接受的prop,所以class被视作穿透attribute,自动透视到<MyButton>的根元素上。
对class和style的合并
//如果一个子组件的根元素已经有了class或style attribute,它会和从父组件上继承的值合并。如果我们将之前的<MyButton>组件的模板改成这样:
<button class="btn">click me</button>
//则最后渲染出的DOM结果会变成:
<button class="btn large">click me </button>
总结:相当于你在父组件声明了class,子组件自己写了一个class,然后这两个class都属于这个组件的样式。然后顺序是子组件的class在前,后是父组件的class.
v-on监听继承
//同样的规则也适用于v-on事件监听器
<MyButton @click="onClick"/>
//click 监听会被添加到<MyButton>的根元素,即那个原生的<button>元素之上。当原生的<button>被点击,会触发父组件的onClike 方法。同样的,如果原生button元素吱声也通过v-on绑定一个事件监听器,则这个监听器个从父组件继承的监听器都会被触发。
总结:点击事件在组件中是继承的
深层组件传承
//有些情况下一个组件会在根节点上渲染另外一个组件。例如,重构一下<MyButton>,让它在根节点上渲染<BaseButton>:
<BaseButton>
//此时<MyBuuton>接收的透传attribute会直接继续传给<BaseButton>
tip:
1.透传的attribute不会包含
<MyButton>上声明过的props或是针对emits声明事件的v-on侦听函数,换句话说,声明过的props和侦听函数被<MyButton>“消费了”。2.透传的attribute若符合声明,也可以作为props传入
<BaseButton>.
- 禁用Attributes
//如果你不想要一个组件自动地继承attribute,你可以在组件选项中设置
inheiAttrs:false
//如果你使用了<script setup>,你细腰一个额外的<script>块来书写这个选项声明:
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
//最常见的需要禁用attribute继承的场景就是attribute需要应用在根节点以外的其他元素上。通过设置inheitAttrs选项为false,你可以完全控制透传进来的attribute被如何使用。
//这些透传进来的attribute可以在模板的表达式中直接用$attrs访问到
<span>Fallthrough attributr:{{$attrs}}</span>
//这个$attrs对象包含了除组件所声明的props和emits之外的所有其他attribute,例如class,style,v-on监听器等等。
tip:
1.和props有所不同,透传attributes在javaScript中保留了他们原始的大小写,所有向foo-bar 这样的一个attribute需要通过$attrs['foo-bar']来访问
2.像@click这样的v-on事件监听器在次对象下被暴露为一个函数$attrs.onClike.
<div class="btn-wrapper">
<button class="btn">click me</button>
</div>
//我们想要所有像class和v-on监听器这样的透传attribute都应用在内部的<button>上而不是在外层的div上,我们可以通过设定inheriAttrs:false和使用v-bind="$attrs"来实现
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
tip:
- 多跟节点的Attributes继承
//和单节点组件有所不同,有着多个根节点的组件没有自动attribute透传行为。如果$attrs没有被显式绑定,将会抛出警告。
<CustomLayout id="custom-layout" @click="changeValue" />
//如果 `<CustomLayout>` 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
<header>...</header>
<main>...</main>
<footer>...</footer>
//如果 `$attrs` 被显式绑定,则不会有警告:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
总结:多组件$attrs显式绑定就不会报错,不然识别不了。
- 在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)
}
}
tip:
需要注意的是,虽然这里的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。
二.插槽
- 1.插槽内容与出口
组件要如何接收模板内容呢?
在某些场景中:
我们想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
//举例FancyButton,使用
<FancyButton>
//插槽内容
click me
</FancyButton>
//而<FancyButton>的模板是这样的
<button class="fany-btn">
//插槽出口
<slot></slot>
</button>
<slot>元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容(slot content)将在slot里渲染。
最终渲染的DOM:
<button class="fancy-btn> click me!</button>
通过使用插槽,<FancyButton>仅负责渲染外层的(以及相应的样式),而其内部的内容由父组件提供。
//理解插槽的另一种方式和下面的javaScript 函数作类比,其概念是类似的
//父元素传入插槽内容
FancyButton('click me!')
//FancyButton 在自己的磨难中渲染插槽内容
funcition FancyButton (slotContent){
return <button class="fancy-btn">
${slotContent}
</button>
}
//插槽内容可以任意合法的模板内容,不局限文本。;如可以传多个元素,甚至是组件
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>
- 2.渲染作用域
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板定义的。
<span>{{messge}}</span>
<FancyButton>{{messge}}</FancyButton>
// 两个{{messge}}差值表达式渲染的内容是一样的。
插槽内容无法访问子组件的数据。vue模板中的表达式只能访问其定义时所处的作用域,这和javaScript的词法作用域规则一样。 父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。
- 3.默认内容
<SubmitButton>
// 如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 `<slot>` 标签之间来作为默认内容:
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
-
4.具名插槽
有时在一个组件中包含多个插槽出口是很有用的。举例来说组件中:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
// 传入不同的内容给不同名字的插槽
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> 渲染插槽内容到对应位置
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
- 5.动态插槽
动态指令参数在v-slot上也是有用的
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
- 6.作用域插槽
在某些场景中:插槽的内容可能想要同时使用父组件域内和子组件域内的数据。 需要让子组件在渲染时将一部分数据提供给插槽。
做法:可以像对组件传递props那样,向一个插槽的出口上传递attributes:
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
当需要接收插槽props时,默认插槽和具名插槽的使用方式有一些小区别。
展示:默认插槽如何接受props对象
通过子组件标签上的 v-slot 指令,直接接收到了一个插槽props对象:
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
子组件传入插槽的props作为了v-slot指令的值,可以在插槽内的表达式中访问。
可以将作用域插槽类比为一个传入子组件的函数。子组件会将相应的props作为参数传给它:
MyComponent({
// 类比默认插槽,将其想成一个函数
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'hello'
return `<div>${
// 在插槽函数调用时传入 props
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
//`v-slot="slotProps"` 可以类比这里的函数签名,和函数的参数类似,我们也可以在 `v-slot` 中
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
- 7.具名作用域插槽
具名作用域插槽的工作方式也是类似的,插槽props可以作为v-slot指令的值被访问到。 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指令将编译错误。这是为了避免因迷人插槽的props的作用域而困惑。
<!-- 该模板无法编译 -->
<template>
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message 属于默认插槽,此处不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>
</template>
为默认插槽使用显式的<template>标签有助于更清晰地支出message属性在其他插槽中不可用:
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
</template>
-
8.高级列表组件示例
什么样的场景才适合用到作用域插槽
它会渲染一个列表,并同时会封装一些在家远程数据的逻辑、使用数据进行列表渲染、或者是像分页活无线滚动这样更进阶的功能,然而我们希望它能够保证足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。
期望的用法:
<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-bing)来传递插槽的props;
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
- 9.无渲染组件
fancylist 同时封装了可重用的逻辑(数据获取,分页等)和视图输出,但也将部分视图输出通过作用域插槽交给了消费者阻止来管理。
拓展:可以想象一下,一些组件可能值包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给消费者组件。我们将这种类类型成为无渲染组件。
// 这里有一个无渲染组件的例子,一个封装了追踪当前鼠标位置逻辑的组件:
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
总结:组件插槽就记住v-slot就ok啦。 3.0写法
//子组件
<div class="dialog__footer" >
<slot name="footer" />
</div>
//父组件
<template v-slot:footer class="dialog-footer">
footer
</template>
三.依赖注入
provide(提供)
要为组件后代提供数据,需要使用provide()函数。
provide()函数接收两个参数, 第一个参数为注入名,
可以是一个字符串或者是一个Symbol,后代组件会用注入明来查找期望注入的值,一个组件可以多次调用provide(),使用不同的注入明,注入不同的依赖值。 第二个参数是提供的值,值可以是任意类型没包括响应式的状态,如ref:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
//如果不适用<script setup>,请确保provide()是在set()同步调用的:
import { provide } from 'vue'
export default {
setup() {
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
}
}
import { ref, provide } from 'vue'
const count = ref(0)
provide('key', count)
//提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
** 应用层Provide**
除了在一个组建中提供依赖,我们还可以在整个应用层面提供依赖:
```js
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假设传入的注入明会被某个祖先链上的组件提供。如果该注入名的却没有任何组件提供,则会派出一个运行时警告。
//如果在注入一个值时不要求必须有提供者,那么我们应该声明默认值,和props类似:
// 如果没有祖先组件提供 "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)
提供将成为对应 Provide 的一个专有概念
四.异步组件
- 基本用法 在大型项目中,我们可能需要拆分应用为更小的快,并仅在需要时再从服务器加载相关组件。Vue提供了defineAsyncComponent方法来实现此功能
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
如你所见,defineAsyncComponents方法接收一个返回Promise的加载函数。这个Promise的resolve回调方法应该在从服务器获得组件定义时调用。你也可以调用reject(rean)表明加载失败、
ES模块动态导入也会返回一个Promise,所以多数情况下我们会将它和defineAsyncComponent搭配使用。类似Vite和webpack这样的构建工具也支持此语法,(并且会将他们作为打包时的代码分割点),因此我们也可以用它来导入Vue单文件组件:
import { defineAsyncComponent } from 'vue'
const AsyncComp = 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
})
-
搭配Suspense使用
异步组件可以搭配内置的
<Suspense>组件一起使用。