组件通信
父子组件通信通过 props
和 $emit
相信小伙伴们都清楚,那么毫无关联的两个组件如果需要实现通信,又有哪些方法呢?相信小伙伴们可能第一时间想到了 vuex
,但是如果只是简单的业务逻辑,vuex
的引入和维护就没有那么必要~~~我百度了一下,大部分有说通过 使用 Vue事件总线(EventBus) 来进行处理无关联组件之间的通信,当然也有说 vue
自身就有自定义事件 event.$on、event.$emit、event.$off
来进行相关逻辑处理,其实两者殊途同归,用法差异不大,来看栗子:
- 初始化 event
// event.js
import Vue from 'vue'
export default new Vue()
- 组件 A,通过
event.$emit
发送事件
<template>
<div class="index">
<button @click="sendMsg()">按钮</button>
</div>
</template>
<script>
import event from './event'
export default {
methods: {
sendMsg() {
event.$emit('aMsg', '来自A页面的消息')
}
}
}
</script>
- 组件 B,通过
event.$on
接收事件
<template>
<div class="list">{{ msg }}</div>
</template>
<script>
import event from './event'
export default {
data() {
return {
msg: ""
}
},
methods: {
addMsgFromA(msg) {
this.msg = msg
console.log(msg) // 来自A页面的消息
}
},
mounted() {
// event.$on('aMsg', (msg) => {
// this.msg = msg
// console.log(msg)
// })
event.$on('aMsg', this.addMsgFromA)
},
beforeDestroy() {
// 及时销毁,否则可能造成内存泄漏
event.$off('aMsg', this.addMsgFromA)
}
}
</script>
上述代码中,特意将组件 B 中的 mounted
函数内的方法进行了封装,而不是直接写,是为了保证在页面销毁时对该方法进行及时移除,我们也可以使用 event.$off('aMsg', this.addMsgFromA)
来移除应用内所有对此某个事件的监听。或者直接调用 event.$off()
来移除所有事件频道,不需要添加任何参数 。
生命周期
Vue
的声明周期基本可以分为三个阶段:
- 挂载阶段
beforeCreate
、created
、beforeMount
、mounted
- 更新阶段
beforeUpdate
、updated
- 销毁阶段
beforeDestroy
、destroyed
created
和mounted
的区别?
created
页面还没有开始渲染,但是页面的实例已经初始化完成(此时可以获取到data
和methods
中的数据,无法获取DOM
节点);mounted
页面完成渲染,此时组件在网页上绘制完成。此时可获取到DOM
节点。
父子组件生命周期调用顺序
写一个简略版的 todoList
,查看父子组件的生命周期调用顺序。
- 父组件
<template>
<div class="index">
<Input @addTitle="addTitleHandler" />
<List :list="list" @delete="deleteHandler" />
</div>
</template>
<script>
import Input from './Input'
import List from './List'
export default {
components: { Input, List },
data() {
return {
list: [
{
id: 'id-1',
title: '标题1'
},
{
id: 'id-2',
title: '标题2'
}
]
}
},
methods: {
addTitleHandler(title) {
this.list.push({
id: `id-${Date.now()}`,
title
})
},
deleteHandler(id) {
this.list = this.list.filter(item => item.id !== id)
}
},
created() {
console.log('index created')
},
mounted() {
console.log('index mounted')
},
beforeUpdate() {
console.log('index beforeUpdate')
},
updated() {
console.log('index updated')
},
beforeDestroy() {
console.log('index beforeDestroy')
},
destroyed() {
console.log('index destroyed')
}
}
</script>
- 子组件 List
<template>
<div class="list">
<ul>
<li v-for="item in list" :key="item.id">
{{ item }}
<button @click.stop="deleteItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return []
}
}
},
methods: {
deleteItem(id) {
this.$emit('delete', id)
}
},
created() {
console.log('list created')
},
mounted() {
console.log('list mounted')
},
beforeUpdate() {
console.log('list beforeUpdate')
},
updated() {
console.log('list updated')
},
beforeDestroy() {
console.log('list beforeDestroy')
},
destroyed() {
console.log('list destroyed')
}
}
</script>
- 子组件 Input
<template>
<div>
<input type="text" v-model="title" />
<button @click="addTitle">add</button>
</div>
</template>
<script>
export default {
data() {
return {
title: ''
}
},
methods: {
addTitle() {
this.$emit('addTitle', this.title)
this.title = ''
}
}
}
</script>
挂载阶段的执行渲染结果为:index created
=> list created
=> list mounted
=> index mounted
,基本可以说明:父子组件渲染的挂载阶段是由外向内在向外进行执行的。而更新阶段渲染结果为:index beforeUpdate
=> list beforeUpdate
=> list updated
=> index updated
,基本逻辑流程和挂载阶段差不多。
$nextTick
Vue
是异步渲染data
改变之后,DOM
不会立刻渲染$nextTick
会在DOM
渲染之后被触发,以获取最新DOM
节点
其实很好理解,因为 Vue
中所有 DOM
的渲染都是异步的,所以我们在 DOM
渲染之后直接获取 DOM
元素可能会有偏差,使用 $nextTick
可以保证所有异步渲染完成之后再来执行,如下栗子:
<template>
<div>
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 获取 DOM 元素
const ulElem = this.$refs.ul1
console.log(ulElem.childNodes.length) // 3
// 使用 $nextTick
this.$nextTick(() => {
const ulElem = this.$refs.ul1
console.log(ulElem.childNodes.length) // 6
})
}
}
}
</script>
slot
- 基本使用 (父组件往子组件中插入一段内容)
// 父组件
<template>
<div id="app">
<slot-demo :url="website.url">
{{ website.title }}
</slot-demo>
</div>
</template>
<script>
import SlotDemo from './components/SlotDemo'
export default {
name: "App",
components: { SlotDemo },
data() {
return {
name: '张三',
website: {
url: 'https://www.baidu.com',
title: '百度',
subTitle: '百度一下,你就知道'
}
}
},
};
</script>
// 子组件
<template>
<div>
<a :href="url">
<slot>默认内容,父组件没设置内容,我就会被显示出来</slot>
</a>
</div>
</template>
<script>
export default {
props: {
url: {
type: String,
default() {
return ''
}
}
}
}
</script>
- 作用域插槽 (子组件
data
中的数据传递给父组件)。子组件通过:slotData
绑定要传递的data
中的数据,父组件通过template
包裹定义v-slot
接收,然后直接插值调用。如下栗子:
// 父组件
<div id="app">
<slot-demo :url="website.url">
<template v-slot="slotProps">
{{ slotProps.slotData.title }}
</template>
</slot-demo>
</div>
<script>
data() {
return {
name: '张三',
website: {
url: 'https://www.baidu.com',
title: '百度',
subTitle: '百度一下,你就知道'
}
}
}
</script>
// 子组件
<div>
<a :href="url">
<slot :slotData="website">{{ website.subTitle }}</slot>
</a>
</div>
<script>
data() {
return {
website: {
url: 'https://www.qq.com',
title: 'QQ',
subTitle: '每一天,乐在沟通'
}
}
}
</script>
- 具名插槽 (父组件通过
v-slot
与子组件slot
中的name
值进行绑定)
// 父组件
<slot-demo>
<template v-slot:header>
<h1>这里的内容会被插入到子组件的 header 中</h1>
</template>
<p>这里的内容会被插入到子组件的 main 中</p>
<template v-slot:footer>
<p>这里的内容会被插入到 footer slot 中</p>
</template>
</slot-demo>
// 子组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
mixin
多个组件有相同的逻辑,抽离出来
mixin
并不是完美的解决方案,会有一些问题变量来源不明确,不利于阅读
多
mixin
可能会造成命名冲突mixin
和组件可能会出现多对多的关系,复杂度较高Vue 3
提出的Composition API
旨在解决这些问题
当然缺点固然很多,但是日常开发中使用 mixin
还是非常符合 真香定律
的。基础用法:
<template>
<div>
<p>{{ name }} {{ major }} {{ city }}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可添加多个,如[myMixin, myMixin1, myMixin2]
data() {
return {
name: '张三',
major: 'web 前端'
}
},
methods: {},
mounted() {
console.log('mixin mounted', this.name)
}
}
</script>
//mixin.js
export default {
data() {
return {
city: '上海'
}
},
methods: {
showName() {
console.log(this.name)
}
},
mounted() {
console.log('mixin.js mounted', this.name) // 先于 vue 模板中的 mounted 执行
}
}
city
和 showName
我们都没有在模板中直接定义,而是定义在 mixin.js
中,但是我们却可以直接使用,这是因为 mixins
会将 mixin.js
中的内容和我们 vue
模板中的内容进行融合,从而导致多个地方都可以直接使用 mixin.js
中的内容。
动态组件
:is = "component-name"
用法需要根据数据,动态渲染的场景。即组件类型不确定。
光说可能有点混,但是开发中还真的遇到过~~~举个栗子:
<template>
<div id="app">
<!-- 使用 :is 和 data 中的值进行绑定 -->
<component :is="nextTickName"></component>
</div>
</template>
<script>
import NextTick from './components/NextTick'
export default {
name: "App",
components: { NextTick }, // 此处还是要引入和注册组件
data() {
return {
nextTickName: NextTick // 将组件赋值到 data 中
}
},
};
</script>
有没有感觉多此一举,但是 vue
将其作为 API
独立出来还是有一定意义的,开发中很多位置还是会用到的,像组件切换,Tab
等确实会用到,具体实用场景小伙伴可自行斟酌哦~~~
异步组件
import() 函数
按需加载,异步加载大组件
感觉这个用法和 vue-router
差不多,具体看栗子一目了然:
<template>
<div id="app">
<next-tick></next-tick>
</div>
</template>
<script>
export default {
name: "App",
components: {
NextTick: () => import('./components/NextTick') // import 按需加载组件
},
data() {
return {
}
},
};
</script>