組件通信
父子組件通信通過 props 和 $emit 相信小伙伴們都清楚,那么毫無關(guān)聯(lián)的兩個(gè)組件如果需要實(shí)現(xiàn)通信,又有哪些方法呢?相信小伙伴們可能第一時(shí)間想到了 vuex,但是如果只是簡(jiǎn)單的業(yè)務(wù)邏輯,vuex 的引入和維護(hù)就沒有那么必要~~~我百度了一下,大部分有說通過 使用 Vue事件總線(EventBus) 來進(jìn)行處理無關(guān)聯(lián)組件之間的通信,當(dāng)然也有說 vue 自身就有自定義事件 event.$on、event.$emit、event.$off 來進(jìn)行相關(guān)邏輯處理,其實(shí)兩者殊途同歸,用法差異不大,來看栗子:
- 初始化 event
// event.js
import Vue from 'vue'
export default new Vue()
- 組件 A,通過
event.$emit發(fā)送事件
<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() {
// 及時(shí)銷毀,否則可能造成內(nèi)存泄漏
event.$off('aMsg', this.addMsgFromA)
}
}
</script>
上述代碼中,特意將組件 B 中的 mounted 函數(shù)內(nèi)的方法進(jìn)行了封裝,而不是直接寫,是為了保證在頁面銷毀時(shí)對(duì)該方法進(jìn)行及時(shí)移除,我們也可以使用 event.$off('aMsg', this.addMsgFromA) 來移除應(yīng)用內(nèi)所有對(duì)此某個(gè)事件的監(jiān)聽?;蛘咧苯诱{(diào)用 event.$off() 來移除所有事件頻道,不需要添加任何參數(shù) 。
生命周期
Vue 的聲明周期基本可以分為三個(gè)階段:
- 掛載階段
beforeCreate、created、beforeMount、mounted - 更新階段
beforeUpdate、updated - 銷毀階段
beforeDestroy、destroyed
created和mounted的區(qū)別?
created頁面還沒有開始渲染,但是頁面的實(shí)例已經(jīng)初始化完成(此時(shí)可以獲取到data和methods中的數(shù)據(jù),無法獲取DOM節(jié)點(diǎn));mounted頁面完成渲染,此時(shí)組件在網(wǎng)頁上繪制完成。此時(shí)可獲取到DOM節(jié)點(diǎn)。
父子組件生命周期調(diào)用順序
寫一個(gè)簡(jiǎn)略版的 todoList ,查看父子組件的生命周期調(diào)用順序。
- 父組件
<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: '標(biāo)題1'
},
{
id: 'id-2',
title: '標(biāo)題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>
掛載階段的執(zhí)行渲染結(jié)果為:index created => list created => list mounted => index mounted,基本可以說明:父子組件渲染的掛載階段是由外向內(nèi)在向外進(jìn)行執(zhí)行的。而更新階段渲染結(jié)果為:index beforeUpdate => list beforeUpdate => list updated => index updated,基本邏輯流程和掛載階段差不多。
$nextTick
Vue是異步渲染data改變之后,DOM不會(huì)立刻渲染$nextTick會(huì)在DOM渲染之后被觸發(fā),以獲取最新DOM節(jié)點(diǎn)
其實(shí)很好理解,因?yàn)?Vue 中所有 DOM 的渲染都是異步的,所以我們?cè)?DOM 渲染之后直接獲取 DOM 元素可能會(huì)有偏差,使用 $nextTick 可以保證所有異步渲染完成之后再來執(zhí)行,如下栗子:
<template>
<div>
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<button @click="addItem">添加一項(xiàng)</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
- 基本使用 (父組件往子組件中插入一段內(nèi)容)
// 父組件
<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>默認(rèn)內(nèi)容,父組件沒設(shè)置內(nèi)容,我就會(huì)被顯示出來</slot>
</a>
</div>
</template>
<script>
export default {
props: {
url: {
type: String,
default() {
return ''
}
}
}
}
</script>
- 作用域插槽 (子組件
data中的數(shù)據(jù)傳遞給父組件)。子組件通過:slotData綁定要傳遞的data中的數(shù)據(jù),父組件通過template包裹定義v-slot接收,然后直接插值調(diào)用。如下栗子:
// 父組件
<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值進(jìn)行綁定)
// 父組件
<slot-demo>
<template v-slot:header>
<h1>這里的內(nèi)容會(huì)被插入到子組件的 header 中</h1>
</template>
<p>這里的內(nèi)容會(huì)被插入到子組件的 main 中</p>
<template v-slot:footer>
<p>這里的內(nèi)容會(huì)被插入到 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
多個(gè)組件有相同的邏輯,抽離出來
mixin并不是完美的解決方案,會(huì)有一些問題變量來源不明確,不利于閱讀
多
mixin可能會(huì)造成命名沖突mixin和組件可能會(huì)出現(xiàn)多對(duì)多的關(guān)系,復(fù)雜度較高Vue 3提出的Composition API旨在解決這些問題
當(dāng)然缺點(diǎn)固然很多,但是日常開發(fā)中使用 mixin 還是非常符合 真香定律 的。基礎(chǔ)用法:
<template>
<div>
<p>{{ name }} {{ major }} {{ city }}</p>
<button @click="showName">顯示姓名</button>
</div>
</template>
<script>
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可添加多個(gè),如[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 執(zhí)行
}
}
city 和 showName 我們都沒有在模板中直接定義,而是定義在 mixin.js 中,但是我們卻可以直接使用,這是因?yàn)?mixins 會(huì)將 mixin.js 中的內(nèi)容和我們 vue 模板中的內(nèi)容進(jìn)行融合,從而導(dǎo)致多個(gè)地方都可以直接使用 mixin.js 中的內(nèi)容。
動(dòng)態(tài)組件
:is = "component-name"用法需要根據(jù)數(shù)據(jù),動(dòng)態(tài)渲染的場(chǎng)景。即組件類型不確定。
光說可能有點(diǎn)混,但是開發(fā)中還真的遇到過~~~舉個(gè)栗子:
<template>
<div id="app">
<!-- 使用 :is 和 data 中的值進(jìn)行綁定 -->
<component :is="nextTickName"></component>
</div>
</template>
<script>
import NextTick from './components/NextTick'
export default {
name: "App",
components: { NextTick }, // 此處還是要引入和注冊(cè)組件
data() {
return {
nextTickName: NextTick // 將組件賦值到 data 中
}
},
};
</script>
有沒有感覺多此一舉,但是 vue 將其作為 API 獨(dú)立出來還是有一定意義的,開發(fā)中很多位置還是會(huì)用到的,像組件切換,Tab 等確實(shí)會(huì)用到,具體實(shí)用場(chǎng)景小伙伴可自行斟酌哦~~~
異步組件
import() 函數(shù)
按需加載,異步加載大組件
感覺這個(gè)用法和 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>