Appearance
组件进阶和插槽
学习目标
- 【掌握】Vue中动态组件
- 【掌握】keep-alive的使用
- 【掌握】Vue中的组件通信
- 【了解】Vue中的ref和$nextTick
- 【掌握】Vue中插槽的基本使用
- 【掌握】Vue中插槽的深入理解
前面已经强调组件系统是Vue的核心,Vue中除开可以通过单文件组件来自定义组件之外,还内置了一批全局组件可以直接使用,接下来我们来学习这些内置组件的使用。
1. 动态组件
有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:
利用vue中的内置组件<component>
可以实现这种动态切换:
vue
<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>
在上述示例中,currentTabComponent
可以包括
- 已注册组件的名字 (绝大多数都采用这种方式)
- 一个组件的选项对象
整体代码如下:
vue
//App.vue
<template>
<div id="app">
<div class="title">
<span @click="fn1">Home</span>
<span @click="fn2">Post</span>
<span @click="fn3">Archive</span>
</div>
<component :is="currentTabComponent" />
</div>
</template>
<script>
import Home from '@/components/Home.vue'
import Post from '@/components/Post.vue'
import Archive from '@/components/Archive.vue'
export default {
name: 'App',
components: {},
data() {
return {
currentTabComponent: Home,
}
},
methods: {
fn1(){
this.currentTabComponent=Home
},
fn2(){
this.currentTabComponent=Post
},
fn3(){
this.currentTabComponent=Archive
}
},
}
</script>
<style scoped>
.title span {
padding: 8px;
background-color: #ccc;
border: 1px solid #ccc;
}
</style>
2.keep-alive
我们刚才使用了<component>
组件实现了组件之间的动态切换,每次切换都会重新执行组件的创建销毁过程,这样既浪费性能又无法缓存住组件的状态,通过<keep-alive>
这个内置组件我们可以解决这个问题,主要使用<keep-alive>
的时候必须保证每个组件都有自己的name
配置项。
vue
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
为了验证效果成功了,我们可以这样做:
将3个组件设置为3个input框,切换组件时input框里的内容被正确保留了。
在每个组件中调用生命周期函数,会发现创建销毁的钩子没有调用,此时会调用2个新的生命周期函数activated和deactivated
3.组件通信
前面我们已经学习了利用props和自定义事件实现父子组件之间的数据通信,现在来对这个话题做进一步探讨:
3.1 父组件—>子组件 props
之前有详细讨论,此处不再赘述。
3.2 子组件—>父组件 $emit()
之前有详细讨论,此处不再赘述。
3.2 父级组件—>后代组件 provide/inject
如果组件的层级变得更加复杂,父组件中包裹子组件,子组件中继续包裹后代组件,这样的通信应该如何完成呢?当然可以利用props逐层传递,但这样又显得太过繁琐,Vue还提供了一组配置选项provide/inject
来实现父级组件向后代组件传递数据。
在父组件中通过一个新的配置选项provide定义了一个数据msg:
vue
provide: {
msg: '父组件中的信息',
}
在所有的后代组件(包括子组件中),可以通过配置项inject来接受这个msg,然后可以直接在模板中使用msg:
vue
inject: ['msg'],
vue
<div class="subchild">
这是后代组件
{{ msg }}
</div>
3.3 任意组件间的通信—>VueX
更多业务场景下,我们希望组件之间没有父子关系的约束可以随意通信,在后面我们会单独来说明如何利用公共状态管理库VueX/Pinia来实现。
除此之外,还有一些通信方式例如 全局事件总线 发布订阅等等,后续如有使用到,我们再专门说明,我们必须熟练掌握props和自定义事件,这是最常用的方式,必须明确一个思想:数据在哪 修改数据的方法就在哪
4.ref
在使用Vue的过程中,我们的思想是操作数据而非DOM结构,但是Vue仍然提供了一个ref
属性来引用DOM结构或者组件。
vue
<!-- 通过this.$refs.p可以访问到这个p标签 -->
<p ref="p">hello</p>
<!-- 通过this.$refs.child可以访问到这个组件实例 -->
<child-component ref="child"></child-component>
5.$nextTick
Vue会在数据更新后进行响应式地视图更新,但是这个更新并非及时的,如果希望将逻辑放到下次视图更新后执行,可以调用$nextTick这个方法来实现,下面用一个案例来说明这一点:
现在希望点击按钮能够切换文本的编辑状态,并且进入到编辑状态时应该能自动获取焦点:
我们可以通过ref获得input输入框,通过focus方法来获得焦点,代码应该如下:
vue
<template>
<div class="app">
<div class="left">
<div
v-if="!isEdit"
class="t1"
>
{{ msg }}
</div>
<div
v-else
class="t2"
>
<input
ref="ref1"
v-model="msg"
type="text"
>
</div>
</div>
<div class="right">
<button @click="edit">
{{ isEdit ? "确定" : "编辑" }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
msg: 'hello',
isEdit: false,
}
},
methods: {
edit() {
this.isEdit = !this.isEdit
if(this.isEdit){
this.$refs.ref1.focus()
}
},
},
};
</script>
<style scoped>
.app {
background-color: #ddd;
padding: 8px;
display: flex;
}
.left {
width: 30%;
margin-right: 20px;
}
.t1 {
border: 1px solid #000;
}
.t2 input {
width: 100%;
}
</style>
此时点击切换会发现聚焦行为并没有发生,控制台也报出了没有获得到DOM元素的错误。
原因就在于在edit方法中执行this.isEdit=!this.isEdit
的时候视图并没有马上更新,input并没有被马上加入到DOM结构了,所以我们应该希望这个聚焦行为发生在DOM结构更新之后,可以用刚才的实例方法$nextTick来实现,对应的代码改写一下:
js
edit() {
this.isEdit = !this.isEdit
this.$nextTick(function(){
if(this.isEdit){
this.$refs.ref1.focus()
}
})
},
聚焦效果正确实现了!
6.插槽基础
通过组件学习,我们已经掌握了如何利用prop向子组件传递数据,现在如果希望传递的是HTML结构呢?Vue给我们提供了插槽这个解决方案,一起来学习下如何使用。
vue
//父组件中的结构
<template>
<div class="app">
<Child>
<h1>这是传递给子组件的标题</h1>
</Child>
</div>
</template>
传递给子组件的HTML结构写在了组件标签之间,之前并没有这样写过,需要留意。
vue
//子组件中的结构
<template>
<div>
这是子组件
<slot></slot>
</div>
</template>
在子组件中通过一个内置组件<slot>
来接受父组件传递过来的结构,最终完成页面渲染。
7.常见的3种插槽
7.1 默认插槽
我们刚刚书写的插槽比较简单,子组件直接接受父组件传递过来的结构渲染即可,我们把这种称之为默认插槽。
7.2 具名插槽
更多时候,我们希望能控制插槽的一些具体行为。比如在父组件中,希望给子组件传递多个结构,在子组件中需要能在不同试图区域渲染这些结构,如果按照默认插槽的机制,显然无法将每个插槽对应起来,这时我们可以通过给插槽命名的方式来实现,这种插槽称为具名插槽,一起来看这个案例:
html
//子组件中的结构
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
</footer>
</div>
现在希望将父组件传递过来的结构进行正确地分发,那么在父组件中,通过v-slot
这个指令来实现:
html
//父组件中的结构
<Child>
<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>
</Child>
同时,在子组件中,通过给<slot>
添加name属性来正确地接受父组件的分发:
html
<div class="container">
<header>
<!-- 我们希望把页头放这里 -->
<slot name='header'></slot>
</header>
<main>
<!-- 我们希望把主要内容放这里 -->
<slot name='default'></slot>
</main>
<footer>
<!-- 我们希望把页脚放这里 -->
<slot name='footer'></slot>
</footer>
</div>
其中,默认插槽的v-slot
指令和name属性可以省略。
7.3 作用域插槽
插槽的本质仍然是父子组件之间的通信,在父组件向子组件分发插槽的过程中,如果父组件要使用子组件中的数据应该如何来做呢?作用域插槽就是解决了这个问题:
vue
//子组件
<template>
<div>
这是子组件
<slot :msg='msg'></slot>
</div>
</template>
<script>
export default {
data() {
return {
msg:'子组件中的信息'//父组件会使用到这个msg
}
},
}
</script>
<style>
</style>
通过在<slot>
上绑定属性的方式,可以将数据传递给父组件。
vue
//父组件
<template>
<div class="app">
<Child>
<template v-slot='msg'>
<h1>{{msg.msg}}</h1>
</template>
</Child>
</div>
</template>
在父组件中通过v-slot='msg'
的形式来接受子组件的数据,msg是可以任意指定的标识符。
作用域也可以理解为子组件向父组件传递数据的一种方式。