【Vue3 基礎(chǔ)】05.組件化
這是 Vue3 + Vite + Pinia +TS + Element-Plus 實戰(zhàn)系列文檔。最近比較忙沒什么時間寫文章,爭取早日把這個系列完結(jié)吧~
生命周期和模板引用
在本章之前,我們通過響應(yīng)式 api 和聲明式渲染,處理了 DOM 的更新,但光是這些,對于一些復(fù)雜的需要手動操作 DOM 的情況,之前介紹的就無法滿足了。
生命周期
每個 Vue 組件在創(chuàng)建時經(jīng)歷的一系列初始化步驟的階段,我們需要在這些階段做額外操作的話,需要調(diào)用對應(yīng)階段的鉤子。
這些階段包括:設(shè)置好數(shù)據(jù)偵聽,編譯模板,掛載實例到 DOM,以及在數(shù)據(jù)改變時更新 DOM 等。Vue 官方給出了圖示,可以幫助我們更好的理解生命周期:

setup:紅色的所有生命周期 API 都在組件的 setup() 階段被同步調(diào)用。
紅色方框:不同階段代表的生命周期,后續(xù)我們寫的 生命周期鉤子 會在此階段執(zhí)行。
主軸上的:代表組件從初始化到卸載的主要事件。
這里我們先簡單介紹,在介紹完生命周期鉤子之后,相信你會更理解這張圖。
生命周期鉤子
了解了上述的生命周期,我們想在對應(yīng)的周期做一些事情的話,在 Vue3 中我們使用 onXxx 的生命周期鉤子,例如:
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
onMounted(() => {
})
onBeforeUnmount(()=>{
})
</script>
如果你使用過 Vue2 的話,你會發(fā)現(xiàn)差別:
<script>
export default {
created() {
},
mounted() {
},
beforeDestroy() {
}
}
</script>
使用方法就如前面寫的,在 setup 中,在生命周期 API 中注入回調(diào)就可以了。這里我們就不去做 Vue2 和 Vue3 的對比了,全當(dāng)新學(xué)的,按照生命周期的順序:
setup:beforeCreate 和 created 被 setup 方法代替。
onBeforeMount():在組件被掛載之前執(zhí)行回調(diào)。組件已經(jīng)完成了其響應(yīng)式狀態(tài)的設(shè)置,但還沒有創(chuàng)建 DOM 節(jié)點。
onMounted():在組件掛載完成后執(zhí)行回調(diào)。通常用于執(zhí)行需要訪問組件所渲染的 DOM 樹相關(guān)的副作用。
onBeforeUpdate():在組件即將因為響應(yīng)式狀態(tài)變更而更新其 DOM 樹之前執(zhí)行回調(diào)。通常用來在 Vue 更新 DOM 之前訪問 DOM 狀態(tài)。
onUpdated():在組件因為響應(yīng)式狀態(tài)變更而更新其 DOM 樹之后執(zhí)行回調(diào)。會在組件的任意 DOM 更新后被調(diào)用,一般用來訪問更新后的 DOM,不能在此做更新 DOM 的操作,可能導(dǎo)致循環(huán)。
onBeforeUnmount():在組件實例被卸載之前執(zhí)行回調(diào)。
onUnmounted():在組件實例被卸載后執(zhí)行回調(diào)。通常用于手動清理一些副作用,例如計時器、DOM 事件監(jiān)聽器或者與服務(wù)器的連接。
onActivated():當(dāng)作為 keep-alive 組件被激活時執(zhí)行回調(diào)。
onDeactivated():當(dāng)作為 keep-alive 組件被取消激活時執(zhí)行回調(diào)。
onErrorCaptured():在捕獲了后代組件傳遞的錯誤時執(zhí)行回調(diào)。通常用來更改組件狀態(tài)來為用戶顯示一個錯誤狀態(tài)。
onRenderTracked()?僅開發(fā)模式使用 ?:當(dāng)組件渲染過程中追蹤到響應(yīng)式依賴時執(zhí)行回調(diào)。通常用于追蹤依賴的調(diào)試。
onRenderTriggered()??僅開發(fā)模式使用 ?:當(dāng)響應(yīng)式依賴的變更觸發(fā)了組件渲染時執(zhí)行回調(diào)。通常用于觸發(fā)更新的調(diào)試。
以上就是所有的生命周期以及其可被調(diào)用的生命周期鉤子,我們在上述鉤子中傳遞回調(diào),Vue 會在其所在的生命周期觸發(fā)。
模板引用 ref
ref 用于注冊元素或者子組件的引用。
模板引用將存儲在與名字匹配的 ref 中,例如想在數(shù)據(jù)加載完之后,更改文字信息或描述信息:
<script setup>
import { ref, onMounted } from "vue";
const h2 = ref(null);
const img = ref(null);
onMounted(() => {
setTimeout(() => {
h2.value.textContent = "數(shù)據(jù)加載完成";
img.value.src = "/src/assets/logo.svg";
}, 3000);
});
</script>
<template>
<h2 ref="h2">數(shù)據(jù)加載中...</h2>
<img ref="img" src="./assets/load.svg" alt="" />
</template>
當(dāng)然我們也完全可以讓上述的代碼中 h2 變成響應(yīng)式的,并在h2中使用 {{}} ?模板語法實現(xiàn)。
組件引用ref
ref 也可以使用在子組件上,相對來說也是較為常見的用法。
首先解釋什么是組件:
在此之前我們都是使用的一個單文件App.vue,如果一個項目將代碼全寫在這一個文件,那將非常難維護(hù),于是我們把可復(fù)用等的頁面組件化,頁面和邏輯抽離,通過導(dǎo)入組件被其它頁面引用。
<script setup>
import Child from './Child.vue'
</script>
<template>
<Child ref="child" />
</template>
使用 ref,父組件能獲取子組件示例:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const child = ref(null)
onMounted(() => {
})
</script>
<template>
<Child ref="child" />
</template>
需要注意的是,子組件沒有使用 <script setup> ?,被引用的組件實例和該子組件的 this 完全一致,父組件擁有對子組件的每個屬性和方法的訪問權(quán)。
如果使用 <script setup> ?那么子組件默認(rèn)私有,除非使用 defineExpose ?顯式暴露。
當(dāng)然在大部分情況下,使用 props 和 emit 就能實現(xiàn)父子組件的交互,而無需使用 ref。
組件傳值 props
組件之間傳值的方式主要可以概括為這三類:父子組件傳值、兄弟組件傳值和遠(yuǎn)親組件傳值。
Vue 提供給我們組件傳值的api有兩種:props、emit。其中我們可以通過 props ?進(jìn)行父=>子組件傳值。
在開發(fā)過程中,我們需要通過 defineProps() 明確子組件的 props。父組件可以像聲明 HTML 參數(shù)一樣傳值,也可以使用 : ?(v-bind 簡寫) 動態(tài)傳值:
<!-- 父組件 -->
<script setup>
import { ref } from "vue";
import Children from "./components/Children.vue";
const hello = ref("Hello");
</script>
<template>
<input v-model="hello" />
<Children msg="hhh" :activeMsg="hello" />
</template>
<!-- 子組件 -->
<script setup>
import { onMounted } from "vue";
const props = defineProps({
msg: String,
activeMsg: String
});
onMounted(() => {
console.log("props", props);
});
</script>
<template>
<span>msg: {{ msg }}</span>
<span>activeMsg: {{ activeMsg }}</span>
</template>
defineProps() 聲明之后,其中的數(shù)據(jù)就可以在子組件模板中使用。在 JavaScript 中訪問則需要通過 ?defineProps() ? 返回的對象訪問。
注意:
props 是只讀的,遵循單項數(shù)據(jù)流,當(dāng)嘗試修改 props 會警告 prop 只讀。
js 中定義數(shù)據(jù)與 props 中重名時,使用的是 js 中定義的。
const activeMsg = "hl";
<span>activeMsg: {{ activeMsg }}</span>
props 提供校驗選項,保證項目沒有使用 TypeScript 進(jìn)行類型檢測,也可以確定一定的數(shù)據(jù)類型,避免不不滿足類型要求的數(shù)據(jù)傳入。(現(xiàn)在 Vue3 支持 TypeScript 可以說這個用處不大了)
interface DataProps {
msg: String;
activeMsg: String;
}
const props = defineProps<DataProps>();
組件監(jiān)聽事件 emit
子組件使用 emit() ?向父組件傳遞數(shù)據(jù)。第一個參數(shù)是事件名稱,其它額外參數(shù)都會被直接傳向父組件的監(jiān)聽器函數(shù)。
父組件使用 @ (v-on)監(jiān)聽子組件時間,并且可以接收子組件傳遞的參數(shù)。
<!-- 父組件 -->
<template>
<Children @response="(msg) => (hello = msg)" />
{{ hello }}// 點擊子組件按鈕后變?yōu)?hello from child
</template>
<script setup>
import { ref } from "vue";
import Children from "./components/Children.vue";
const hello = ref("Hello");
</script>
<!-- 子組件 -->
<template>
<button @click="emit('response', 'hello from child')">emit</button>
</template>
<script setup>
const emit = defineEmits(["response"]);
</script>
注意:
在模板中可以使用 $emit 的語法,js中只能使用 defineEmits 返回對象 emit ?觸發(fā)事件。
可以使用類型標(biāo)注觸發(fā)事件,對觸發(fā)的事件有更精準(zhǔn)的控制。
const emit = defineEmits<{
(e: 'response', msg: string): void
}>()
傳遞模板-插槽 slot
除了傳遞數(shù)據(jù)外,父組件還可以通過插槽 slot ?的方式將模板傳遞給子組件。
<!-- 父組件 -->
<template>
<Children>slot button content</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子組件 -->
<template>
<button><slot /></button> ?
</template>
<script setup></script>
默認(rèn)內(nèi)容
如果想設(shè)置默認(rèn)內(nèi)容的話,比如在父組件不向子組件傳遞模板字符,而使用子組件按鈕內(nèi)容有默認(rèn) content:
<!-- 父組件 -->
<template>
<Children></Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子組件 -->
<template>
<button>
<slot>
content
</slot>
</button>
</template>
<script setup></script>
具名插槽
如果組件包含多個插槽出口,則需要使用具名插槽,用來給插槽一個唯一 ID,以確定不同出口要渲染的內(nèi)容。
<!-- 父組件 -->
<template>
<Children>
<template v-slot:header> Header </template>
<template v-slot:button> slot button content </template>
</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<template>
<div><slot name="header" /></div>
<button><slot name="button" /></button>
</template>
<script setup></script>
v-slot 可以簡寫為 # ,v-slot:header => #header。并且v-slot 也可以接受動態(tài)參數(shù)(動態(tài)插槽名): #[dynamicSlotName] 。
作用域插槽
上述的幾種插槽,無法訪問到子組件的狀態(tài),在某些場景中我們想要子組件傳遞數(shù)據(jù)給插槽,作用域插槽就可以滿足這個需求。
<!-- 父組件 -->
<template>
<Children>
<template v-slot:header="slotProps"> {{ slotProps.msg }}</template>
</Children>
</template>
<script setup>
import Children from "./components/Children.vue";
</script>
<!-- 子組件 -->
<template>
<input type="text" v-model="msg" />
<div><slot name="header" :msg="msg" /></div>
</template>
<script setup>
import { ref } from "vue";
const msg = ref("");
</script>
準(zhǔn)確來說,上述例子是具名作用域插槽。如果是普通的作用域插槽,即改變 template 的具名插槽為普通插槽就可以了。
實戰(zhàn)
前幾節(jié)講到了組件的各生命周期鉤子、組件傳值的 props、觸發(fā)事件的 emit 以及模板插槽 slot,用的例子比較簡單,我們通過實戰(zhàn)體驗一下在實戰(zhàn)開發(fā)中的運用,幫助我們更好的理解和運用。
博客的列表展示功能,提供分類功能。
分類的數(shù)據(jù)來源于父組件,其選擇的類型通過回調(diào)告知父組件。
列表部分父組件做元素內(nèi)容和樣式的控制,子組件做列表的基礎(chǔ)循環(huán)等的通用操作。
示例盡可能的包含到我們這章所學(xué)知識,如下述代碼部分看不懂,代表你那部分知識還沒理解清楚。
本節(jié)中的例子里包含部分ts的類型確認(rèn)
父組件?App.vue:
<script setup>
import { reactive, ref, onMounted } from "vue";
import Children from "./components/Children.vue";
import ClassifyHeader from "./components/ClassifyHeader.vue";
const tags = reactive({
list: ["vue", "react"],
checked: [],
});
function checkedTags(checked) {
tags.checked = checked;
getData();
}
onMounted(() => {
getData();
});
const listRef = ref();
function getData() {
const params = { tags: tags.checked, page: 1 };
console.log("請求參數(shù):", params);
setTimeout(() => {
const data = [
{ title: "JavaScript 入門到精通", username: "Chocolate 1999", date: "2023-02-11" },
{ title: "Vue3 實戰(zhàn)", username: "HearLing", date: "2023-03-09" },
];
listRef.value.loadData(data);
}, 1000);
}
</script>
<template>
<ClassifyHeader :tags="tags.list" @select="checkedTags" />
<Children ref="listRef">
<template #item="{ title, username, date }">
<div>
<p>{{ title }}</p>
<p>作者:{{ username }} | 時間:{{ date }}</p>
</div>
</template>
</Children>
</template>
<style scoped></style>
父組件涉及知識點:
與 ClassifyHeader ?分類組件的傳值。props 和 emit。
生命周期 onMounted。在組件掛載后請求數(shù)據(jù)。
組件引用 ref 。使用子組件的方法。
具名作用域插槽。
父組件和分類組件
在父組件中,初始的?tags.list?值通過 props 傳遞給子組件,同時監(jiān)聽了 select 事件。select 觸發(fā)會執(zhí)行 checkedTags 函數(shù),父組件得到 checked 值,并做出重新請求列表數(shù)據(jù)的操作。
components/ClassifyHeader.vue?分類組件:
<!-- 分類 -->
<template>
<div v-for="item in props.tags">
<input type="checkbox" :value="item" @click="select(item)" />
{{ item }}
</div>
</template>
<script setup>
const props = defineProps(["tags"]);
const emit = defineEmits<{
(e: "select", checked: string[]): void;
}>();
const checked: string[] = [];
function select(item) {
const index = checked.indexOf(item);
if (index !== -1) {
checked.splice(index, 1);
} else {
checked.push(item);
}
emit("select", checked);
}
</script>
<style scoped></style>
ClassifyHeader ?分類組件主要就是記錄所選類別,并通過 emit 傳遞數(shù)據(jù)給父組件。
父組件與列表組件
父組件通過 ref 獲取到子組件示例,并且使用子組件暴露的 loadData 方法加載數(shù)據(jù)。子組件通過作用域插槽,傳遞數(shù)據(jù)給父組件,父組件控制內(nèi)容布局。
components/Children.vue?列表組件:
<!-- 子組件 -->
<script setup>
import { ref } from "vue";
interface Item {
title: string;
username: string;
date: string;
}
const items = ref<Item[]>([]);
const loadData = (data) => {
items.value = data;
};
defineExpose({
loadData,
});
</script>
<template>
<ul>
<li v-if="!items.length">Loading...</li>
<li v-for="item in items">
<slot name="item" v-bind="item" />
</li>
</ul>
</template>
<style scoped></style>
子組件,提供具名插槽 slot 以及用 defineExpose 拋出 loadData 方法供父組件通過組件示例拿到。
上述例子,我們就把大部分的知識都重新實戰(zhàn)復(fù)習(xí)了一遍,建議可以把這幾個代碼自己寫一遍,加深印象。
其它傳值方式
除了上述的 props、emit 父子組件傳值之外。還可以使用依賴注入 API :provide、inject。
provide():提供一個值,可以被后代組件注入。使用方式:provide(/* 注入名?/ 'message', /?值 */ 'hello!') 。
inject():注入上層組件提供的數(shù)據(jù)。使用方式:const message = inject('message') 。
兄弟組件,可以可以通過父組件控制數(shù)據(jù)傳值。
對于跨組件通信,我們可以使用狀態(tài)管理工具,比如我們后面要學(xué)的 Pinia,就是一個狀態(tài)管理框架。
總結(jié)
本章中,我們首先結(jié)合 Vue 的生命周期的流程圖,例舉了各個生命周期鉤子的觸發(fā)時機,以及部分鉤子的使用場景。然后講到了 ref 的作用,最后講完并實戰(zhàn)了組件通信相關(guān)的 api。
至此 Vue3 的基礎(chǔ)知識到這里已經(jīng)結(jié)束了,還剩下一小部分,留在我們實戰(zhàn)課程中探索。
如果未學(xué)習(xí)過TypeScript的話,我也準(zhǔn)備了 TypeScript 相關(guān)的入門基礎(chǔ)知識,課程不會很長,帶大家了解 TypeScript 常用的一些知識,為實戰(zhàn)做準(zhǔn)備。