Vue3官网-深入组件(三)父子组件、插槽(<slot>、作用域插值、具名插槽缩写)、 Provide / Inject(响应式)、动态组件keep-alive 、异步组件 defineAsyncComponent
总结:
-
补充
-
Vue (读音 /vju?/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。 -
truthy(真值):在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false 、0 、"" 、null 、undefined 和 NaN 以外皆为真值)。括号内都是假值falsy。 -
.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault() -
==Event.preventDefault方法取消浏览器对当前事件的默认行为。==比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。 -
vue:用组件(app.component)构建一个模板(template),并反复使用模板 -
父组件、子组件
-
const app = Vue.createApp({
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})
-
上面代码中app为父组件,ComponentA和ComponentB为子组件 -
插槽<slot> <todo-button>(内容分发),
-
Provide / Inject -
当我们需要从父组件向子组件传递数据时,我们使用 props。有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。 -
我们可以使用一对 provide 和 inject 。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。 -
访问组件实例 property
-
如果我们尝试在此处 provide 一些组件的实例 property,这将是不起作用的: provide: {
todoLength: this.todos.length
},
-
要访问组件实例 property,我们需要将 provide 转换为返回对象的函数 provide() {
return {
todoLength: this.todos.length
}
-
响应式
-
如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property: app.component('todo-list', {
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
-
动态组件&异步组件
- 动态组件
keep-alive
- 曾经在一个多标签的界面中使用
is attribute 来切换不同的组件 - 如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的
currentTabComponent 实例。 - 那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个
<keep-alive> 元素将其动态组件包裹起来。 - 异步组件
defineAsyncComponent
- 在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。
1. 插槽<slot> <todo-button>
该页面假设你已经阅读过了组件基础。如果你对组件还不太了解,推荐你先阅读它。
插槽内容
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。
它允许你像这样合成组件:
<todo-button>
Add todo
</todo-button>
然后在 <todo-button> 的模板中,你可能有:
<button class="btn-primary">
<slot></slot>
</button>
当组件渲染的时候,<slot></slot> 将会被替换为“Add Todo”。
<button class="btn-primary">
Add todo
</button>
不过,字符串只是开始!插槽还可以包含任何模板代码,包括 HTML:
<todo-button>
<i class="fas fa-plus"></i>
Add todo
</todo-button>
或其他组件
<todo-button>
<font-awesome-icon name="plus"></font-awesome-icon>
Add todo
</todo-button>
如果 <todo-button> 的 template 中没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃
<button class="btn-primary">
Create a new item
</button>
<todo-button>
Add todo
</todo-button>
渲染作用域
当你想在一个插槽中使用数据时,例如:
<todo-button>
Delete a {{ item.name }}
</todo-button>
该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)。
插槽不能访问 <todo-button> 的作用域。例如,尝试访问 action 将不起作用:
<todo-button action="delete">
Clicking here will {{ action }} an item
</todo-button>
请记住这条规则:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
备用内容
有时为一个插槽设置具体的备用 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:
<button type="submit">
<slot></slot>
</button>
我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为备用内容,我们可以将它放在 <slot> 标签内:
<button type="submit">
<slot>Submit</slot>
</button>
现在当我们在一个父级组件中使用 <submit-button> 并且不提供任何插槽内容时:
<submit-button></submit-button>
备用内容“Submit”将会被渲染:
<button type="submit">
Submit
</button>
但是如果我们提供内容:
<submit-button>
Save
</submit-button>
则这个提供的内容将会被渲染从而取代备用内容:
<button type="submit">
Save
</button>
具名插槽
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:
<div class="container">
<header>
</header>
<main>
</main>
<footer>
</footer>
</div>
对于这样的情况,<slot> 元素有一个特殊的 attribute:name 。这个 attribute 可以用来定义额外的插槽:
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
一个不带 name 的 <slot> 出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<template v-slot:default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
现在 <template> 元素中的所有内容都将会被传入相应的插槽。
渲染的 HTML 将会是:
<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>
注意,v-slot 只能添加在 <template> 上 (只有一种例外情况)
作用域插槽
有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。
例如,我们有一个组件,包含 todo-items 的列表。
app.component('todo-list', {
data() {
return {
items: ['Feed a cat', 'Buy milk']
}
},
template: `
<ul>
<li v-for="(item, index) in items">
{{ item }}
</li>
</ul>
`
})
我们可能会想把 {{ item }} 替换为 <slot> ,以便在父组件上自定义。下面的内容是写在父组件中的:
<todo-list>
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
但是,这是行不通的,因为只有 <todo-list> (父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。)组件可以访问 item ,我们将从其父组件提供插槽内容。
要使 item 可用于父级提供的插槽内容,我们可以添加一个 <slot> 元素并将其作为一个 attribute 绑定:
<ul>
<li v-for="( item, index ) in items">
<slot :item="item"></slot>
</li>
</ul>
可以根据自己的需要将很多的 attribute 绑定到 slot 上。
<ul>
<li v-for="( item, index ) in items">
<slot :item="item" :index="index" :another-attribute="anotherAttribute"></slot>
</li>
</ul>
绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
</todo-list>
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps ,但你也可以使用任意你喜欢的名字。
独占默认插槽的缩写语法
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:
<todo-list v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</todo-list>
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
<todo-list v-slot="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</todo-list>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:
<todo-list>
<template v-slot:default="slotProps">
<i class="fas fa-check"></i>
<span class="green">{{ slotProps.item }}</span>
</template>
<template v-slot:other="otherSlotProps">
...
</template>
</todo-list>
解构插槽 Prop
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
function (slotProps) {
}
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<todo-list v-slot="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 item 重命名为 todo :
<todo-list v-slot="{ item: todo }">
<i class="fas fa-check"></i>
<span class="green">{{ todo }}</span>
</todo-list>
你甚至可以定义备用内容,用于插槽 prop 是 undefined 的情形:
<todo-list v-slot="{ item = 'Placeholder' }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
动态插槽名
动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
具名插槽的缩写
跟 v-on 和 v-bind 一样,v-slot 也有缩写,==即把参数之前的所有内容 (v-slot: ) 替换为字符 # 。==例如 v-slot:header 可以被重写为 #header :
<base-layout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<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>
</base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<todo-list #="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
<todo-list #default="{ item }">
<i class="fas fa-check"></i>
<span class="green">{{ item }}</span>
</todo-list>
2. Provide / Inject
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide 和 inject 。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。
例如,我们有这样的层次结构:
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
如果要将 todo-items 的长度直接传递给 TodoListStatistics ,我们要将 prop 逐级传递下去:TodoList -> TodoListFooter -> TodoListStatistics 。通过 provide/inject 方法,我们可以直接执行以下操作:
const app = Vue.createApp({})
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
user: 'John Doe'
},
template: `
<div>
{{ todos.length }}
<!-- 模板的其余部分 -->
</div>
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(`Injected property: ${this.user}`)
}
})
但是,如果我们尝试在此处 provide 一些组件的实例 property,这将是不起作用的:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
todoLength: this.todos.length
},
template: `
...
`
})
要访问组件实例 property,我们需要将 provide 转换为返回对象的函数
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
},
template: `
...
`
})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。
实际上,你可以将依赖注入看作是“long range props”,除了:
- 父组件不需要知道哪些子组件使用它 provide 的 property
- 子组件不需要知道 inject 的 property 来自哪里
处理响应性
在上面的例子中,如果我们更改了 todos 的列表,这个变化并不会反映在 inject 的 todoLength property 中。这是因为默认情况下,provide/inject 绑定并不是响应式的。我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property:
app.component('todo-list', {
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`)
}
})
在这种情况下,任何对 todos.length 的改变都会被正确地反映在注入 todoLength 的组件中。在响应式计算和侦听和组合式 API 部分中阅读更多关于 reactive provide/inject 的信息。
3. 动态组件 & 异步组件
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
在动态组件上使用 keep-alive
我们之前曾经在一个多标签的界面中使用 is attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复渲染导致的性能问题。例如我们来展开说一说这个多标签界面:
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
来看看修改后的结果:
现在这个 Posts 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。你可以在这个示例查阅到完整的代码。
你可以在 API 参考查阅更多关于 <keep-alive> 的细节。
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 有一个 defineAsyncComponent 方法:
const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '<div>I am async!</div>'
})
})
)
app.component('async-example', AsyncComp)
如你所见,此方法接受返回 Promise 的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve 回调。你也可以调用 reject(reason) ,来表示加载失败。
你也可以在工厂函数中返回一个 Promise ,把 webpack 2 和 ES2015 语法相结合后,我们就可以这样使用动态地导入:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
app.component('async-component', AsyncComp)
当在局部注册组件时,你也可以使用 defineAsyncComponent
import { createApp, defineAsyncComponent } from 'vue'
createApp({
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
})
与 Suspense 一起使用
异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 <Suspense> ,它将被视为该 <Suspense> 的异步依赖。在这种情况下,加载状态将由 <Suspense> 控制,组件自身的加载、错误、延迟和超时选项都将被忽略。
异步组件可以选择退出 Suspense 控制,并可以在其选项中指定 suspensible:false ,让组件始终控制自己的加载状态。
你可以在 API 参考查看更多可用的选项。
|