文章目录
- Vue的组件嵌套
- Vue组件化-组件间通信
- 父子组件之间通信
- 非父子组件间的通信
Vue的组件嵌套
前面我们是将所有的逻辑放到一个App.vue中:
- 在之前的案例中,我们只是
创建了一个组件App
; - 如果我们一个应用程序
将所有的逻辑都放在一个组件中
,那么这个组件就会变成非常的臃肿和难以维护
; - 所以组件化的核心思想应该是
对组件进行拆分
,拆分成一个个小的组件
; - 再将
这些组件组合嵌套在一起
,最终形成我们的应用程序
;
观察以下代码,我们可以发现:
- 将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。
- 并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。
- 所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件。
<template>
<div id="app">
<div>
<h2>我是标题</h2>
<div class="top">topbanner</div>
<div class="nav">nav</div>
</div>
<div>
<h2>content</h2>
<ul>
<li>商品列表1</li>
<li>商品列表2</li>
<li>商品列表3</li>
<li>商品列表4</li>
<li>商品列表5</li>
</ul>
</div>
<div>
<h2>Footer</h2>
<h2>免责声明</h2>
</div>
</div>
</template>
大体上,我们可以按照如下方式拆分:
像上面的代码通过拆分后,我们只需要在App根组件中去编写对应的组件:
<template>
<div id="app">
<!-- 组件嵌套:形成组件数,组件中又有局部组件,各个父子组件关系 -->
<app-header></app-header>
<AppContent></AppContent>
<AppFotter></AppFotter>
</div>
</template>
<script>
import AppHeader from "./components/AppHeader.vue";
import AppContent from "./components/AppContent.vue";
import AppFotter from "./components/AppFooter.vue";
export default {
components:{
AppHeader,
AppContent,
AppFotter,
}
}
</script>
<style scoped>
</style>
上面的嵌套逻辑有如下关系:
- App组件是Appheader组件,AppContent组件,AppFooter组件的
父组件
; - Main组件是Banner、ProductList组件的
父组件
; - 还可以将AppHeader组件中的top和nav部分抽取为子组件;
在开发过程中,我们会经常遇到需要组件之间相互进行通信:
- 比如App可能
使用了多个Header
,每个地方的Header展示的内容不同
,那么我们就需要使用者传递给Header一些数据
,让其进行展示; - 又比如我们在Main中一次性
请求了Banner数据和ProductList
数据,那么就需要传递给它们
来进行展示; - 也可能是
子组件中发生了事件
,需要由父组件来完成某些操作
,那就需要子组件向父组件传递事件
;
总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们就具体学习一下组件之间是如何相互之间传递数据的:
Vue组件化-组件间通信
父子组件之间通信
(1)父传子:父组件可以通过props向子组件传递属性,然后子组件可以在其模板中使用这些属性。
Props方式一:数组用法
例如我们有如下的一个根组件, 用于展示用户信息, 但是由于子组件中的数据时固定的, 我们展示的两个信息是相同的:
- 这时候就需要父组件通过自定义的attribute, 将数据传递给子组件:
<template>
<!-- 父传子:传递参数 -->
<!-- 展示信息-->
<ShowInfo name="kobe" age="25" height="1.89"></ShowInfo>
</template>
<script>
import ShowInfo from "../02_组件通信父传子/ShowInfo.vue";
export default {
components:{
ShowInfo
},
}
</script>
- 父组件通过自定义attribute将数据传递给子组件, 子组件可以通过props接收:
<template>
<!-- 1.展示sevgilid的个人信息 -->
<div class="infos">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>身高:{{ height }}</h2>
</div>
</template>
<script>
export default {
// 接收父组件传递过来的参数
// 1.props数组语法
// -弊端:(1)不能对类型进行验证(2)没有默认值
props:["name","age","height"]
}
</script>
props父传子数组方式效果示例:
Props方式二:对象用法
在数组用法中我们只能说明传入的attribute的名称,并不能对其进行任何形式的限制
,
当使用对象语法的时候,我们可以对传入的内容限制更多(重点掌握):
- 比如通过type指定传入的attribute的类型;
- 比如通过require指定传入的attribute是否是必传的;
- 比如通过default指定没有传入时,attribute的默认值;
<template>
<!-- 1.父传子:传递参数 -->
<!-- 为age加上:将父组件的age属性绑定到子组件的age属性上 -->
<ShowInfo name="kobe" :age="25" height="1.89" abc="bca"></ShowInfo>
<!-- 如果当前的属性是一个非props的attribute(属性),那么该属性会默认添加到子组件的根元素上:如此处的abc -->
<ShowInfo name="ysl" :age="23" height="1.68"></ShowInfo>
<!-- 2.不传递参数时展示默认信息: -->
<ShowInfo></ShowInfo>
</template>
<script>
import ShowInfo from "../02_组件通信父传子/ShowInfo.vue";
export default {
components:{
ShowInfo
},
}
</script>
- 子组件中使用对象的方式接收父组件传递的数据:
<template>
<!-- 1.展示sevgilid的个人信息 -->
<div class="infos">
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>身高:{{ height }}</h2>
</div>
</template>
<script>
export default {
// 2.props对象语法(掌握)
props: {
name: {
type: String,
// 必传参,加上之后
// required:true,
default: "我是默认name"
},
age: {
type: Number,
default: 0
},
height: {
type: Number,
default: 2
},
friend: {
type: Object,
// 如果是对象类型,这里必须是函数
default: () => ({ name: "james" })
},
hobbies: {
// 数组类型,默认值也写为函数
type: Array,
default() {
return ["唱", "跳", "rap"]
}
}
}
}
</script>
props父传子对象方式效果示例:
补充一:
type可以指定多种类型:String,Number,Boolean,Array,Object,Date,Function,Symbol;
补充二:
当type指定类型为对象类型和数组类型时,默认值需要写为一个函数,如下:
Props:{
friend: {
type: Object,
// 如果是对象类型,这里必须是函数
default: () => ({ name: "james" })
},
}
(2)子传父:子组件可以使用$emit向父组件发送事件
,然后父组件可以侦听这些事件并对它们做出响应。
使用场景:
- 当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;
- 子组件有一些内容想要传递给父组件的时候;
例如:
- 我们可以
在子组件中创建一个自定义事件
,然后在触发时使用emit将数据发送
给父组件。
<template>
<button @click="handleClick">Click me</button>
</template>
<script>
export default {
methods: {
handleClick() {
this.$emit('button-clicked', 'Hello, World!')
}
}
}
</script>
- 在
父组件中,我们可以使用@事件名来侦听
这个事件,然后在父组件的方法中处理这些数据:
<template>
<my-button @button-clicked="handleButtonClicked"></my-button>
</template>
<script>
import MyButton from './MyButton.vue'
export default {
components: {
MyButton
},
methods: {
handleButtonClicked(data) {
console.log(data) // 输出 Hello, World!
}
}
}
</script>
再写一个计数器案例来练习子传父的操作:
- 计数器可以+1, +5, +10, -1, -5, -10, 我们将加的功能抽离成一个组件, 将减的功能抽离一个组件
- 当我们子组件发生了加或者减的事件, 如何让父组件监听到呢, 这时候就需要使用子组件发生自定义事件给父组件监听
- 子组件事件触发之后, 通过 this.$emit的方式发出去事件;
addBtn.vue:
<template>
<div class="add">
<button @click="addClick(1)">+1</button>
<button @click="addClick(5)">+5</button>
<button @click="addClick(10)">+10</button>
</div>
</template>
<script>
export default {
methods:{
addClick(count){
console.log(count);
this.$emit("add",count)
}
}
}
</script>
subBtn.vue:
<template>
<div class="sub">
<button @click="subClick(1)">-1</button>
<button @click="subClick(5)">-5</button>
<button @click="subClick(10)">-10</button>
</div>
</template>
<script>
export default {
// // 1.emits的数组语法:自定义事件时,在此处注册说明一下,再见他事件时就有提示
// emits:["sub"]:常用,
// 2.emits的对象语法(vue3当中可以对传递的参数就行验证(了解))
emits: {
sub: function (count) {
if (count < 10) {
return true
}
return false
}
},
methods: {
subClick(count) {
console.log(count);
// 让子组件发出去一个自定义事件(事件名称,参数)
this.$emit("sub", count)
}
}
}
</script>
App.vue:父组件监听子组件发出的自定义事件, 然后执行对应的操作
<template>
<div id="app">
<h2>计数器:{{ counter }}</h2>
<add-btn @add="addCounter"></add-btn>
<SubBtn @sub="subCounter"></SubBtn>
</div>
</template>
<script>
import AddBtn from "./components/AddBtn.vue";
import SubBtn from "./components/SubBtn.vue";
export default {
data() {
return {
counter: 0
}
},
components: {
AddBtn,
SubBtn
},
methods: {
addCounter(count) {
this.counter += count;
},
subCounter(count) {
this.counter -= count;
}
}
}
</script>
补充:
- 在Vue3中,提供了一个emits API;
emits选项是一个数组
,用于在组件中注册自定义事件。 - 当子组件触发自定义事件时,父组件可以接收到事件并做出响应。
声明emits选项后,会为组件自动添加$emit方法
,并在使用时进行类型检查和语法提示,方便使用和开发。 - 例如,我们可以在组件中声明一个emits选项,然后在模板中使用它触发一个名为"test"的事件:
export default {
emits: ['test'], // 声明事件名称
methods: {
handleClick() {
this.$emit('test', 'Hello, World!') // 触发test事件
}
}
}
在模板中触发test事件时,代码编辑器会自动提示出test事件名称,方便开发者使用。
非父子组件间的通信
- 此章讲解的是, 在学习状态管理Vuex和Pinia之前, 非父子间通信的方案;
- Vuex和Pinia是Vue官方提供的状态管理库,用于管理应用程序中的所有数据
在开发中,我们构建了组件树之后,除了父子组件之间的通信之外,还会有非父子组件之间的通信
。
这里我们主要讲两种方式:
Provide/Inject
;全局事件总线
;
(1)Provide和Inject
Provide/Inject用于非父子组件之间共享数据:
- 比如有一些
深度嵌套
的组件,子组件想要获取父组件的部分
内容; - 在这种情况下,如果我们仍然将
props沿着组件链逐级传递下去
,就会非常的麻烦
;
对于这种情况下,我们可以使用 Provide 和 Inject :
-
无论层级结构有多
深,父组件都可以作为其所有子组件
的依赖提供者; -
父组件有
一个 provide 选项
来提供数据; -
子组件有
一个 inject 选项
来开始使用这些数据;
在父组件中,我们可以通过provide提供数据,例如:
export default {
provide: {
message: 'Hello, World!'
}
}
在子组件中,通过inject注入数据:
export default {
inject: ['message'],
created() {
console.log(this.message) // 输出"Hello, World!"
}
}
总结:
provide数据
传出对应的是一个对象
,inject接收
祖先传递的数据对应的是一个数组
- 通过provide和inject,我们可以将数据提供给组件树中的任何一个组件,而不仅仅是父子组件之间。
provide和inject
在使用时的一个重要问题是数据不是响应式的
,也就是说,当提供的数据发生变化时,子组件不会自动更新。- 可以简单的看作“long range props”,除了:
- 父组件不需要知道哪些子组件使用它 provide 的 property;
- 子组件不需要知道 inject 的 property 来自哪里
处理响应式数据:
让provide提供的数据变成响应式:
- 使用响应式的一些API来完成这些功能,比如
computed函数
; - 这个
computed是vue3的新特性
,这里以先直接使用,后续会专门讲解;
例如,在父组件中,我们提供了一个设置主题的方法和一个存储主题的值:
export default {
data() {
return {
theme: 'light'
}
},
provide() {
return {
setTheme: this.setTheme,
themeState: computed(() => this.theme)
}
},
methods: {
setTheme(value) {
this.theme = value
}
}
}
在上述代码中,我们使用computed将theme属性包装到themeState属性中,当theme属性变化时,themeState会自动更新并通知子组件,这样子组件就可以观察到themeState的变化并更新UI了。
然后在子组件中,我们通过inject注入themeState和setTheme方法,并使用themeState属性来渲染UI。
export default {
inject: ['themeState', 'setTheme'],
computed: {
theme() {
return this.themeState.value
}
}
}
需要注意的是,由于使用了computed
,我们不能直接使用提供的数据,而是需要通过包装后的computed到value属性
来访问它们的值
。
总之,当使用provide和inject进行组件间通信时,为了确保数据的更新能够触发UI的更新,我们需要使用computed属性包装提供的数据。
(2)事件总线
Vue3从实例中移除了 o n 、 on、on、off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
- Vue3官方有推荐一些库,例如 mitt 或 tiny-emitter, 这两个库虽然不再维护, 但还是可以使用的;
- 这里我们使用coderwhy老师自己封装的
hy-event-store
三方库
使用步骤:
- 安装这个库:
npm install hy-event-store
- 封装一个工具eventbus.js:
import { HyEventBus } from "hy-event-store";
const eventBus = new HyEventBus()
export default eventBus
- 在项目中导入并引用:
在App.vue中监听事件;
在content.vue中触发事件;
content中触发事件:
<template>
<div class="home-content">
<button @click="btnClick">按钮</button>
</div>
</template>
<script>
import eventBus from './utils/event-bus'
export default {
methods: {
btnClick() {
console.log("myEvent事件被监听")
// 发送事件到事件总线上
eventBus.emit("myEvent", "sevgilid", 19, 1.86)
}
},
}
</script>
App中监听事件:
<script>
import eventBus from './utils/event-bus'
import Home from './Home.vue'
export default {
components: {
Home
},
created() {
// 监听事件总线上的事件
eventBus.on("myEvent", (name, age, height) => {
console.log(name, age, height)
})
},
}
</script>