欢迎来到前端面试通关指南专栏!从js精讲到框架到实战,渐进系统化学习,坚持解锁新技能,祝你轻松拿下心仪offer。
前端面试通关指南专栏主页
前端面试专栏规划详情
在Vue3的开发体系中,组件通信与生命周期机制是构建高效、可维护应用的关键。掌握这些核心知识,能帮助开发者更好地组织代码结构,实现组件间的协同工作。接下来,我们将深入剖析Vue3组件通信的多种方式以及组件生命周期的各个阶段。
Props和Emits是Vue组件间通信的两个核心机制,构成了父子组件数据交互的基础模式。
Props是单向数据流的实现方式,父组件通过属性绑定的方式将数据传递给子组件。在Vue 3的语法中,使用
defineProps
宏来声明和验证props:
<template>
<div class="article-card">
<h2>{{ title }}h2>
<p v-if="description">{{ description }}p>
<span class="views">{{ views }}次浏览span>
div>
template>
<script setup>
const props = defineProps({
// 必传的字符串类型
title: {
type: String,
required: true,
validator: value => value.length <= 50 // 自定义验证
},
// 可选的对象类型
meta: {
type: Object,
default: () => ({})
},
// 带默认值的数字
views: {
type: Number,
default: 0
},
// 可选描述
description: String // 简写形式
});
script>
Emits允许子组件向父组件发送自定义事件,实现子到父的通信。在Vue 3中建议使用defineEmits
进行明确的事件声明:
<template>
<div class="search-box">
<input
v-model="keyword"
@keyup.enter="submitSearch"
placeholder="请输入关键词..."
/>
<button @click="clearInput">清空button>
div>
template>
<script setup>
import { ref } from 'vue';
const emit = defineEmits({
// 带验证的事件
search: (payload) => {
if (!payload || payload.length < 2) {
console.warn('搜索关键词至少2个字符');
return false;
}
return true;
},
// 简单事件
clear: null
});
const keyword = ref('');
const submitSearch = () => {
emit('search', keyword.value.trim());
};
const clearInput = () => {
keyword.value = '';
emit('clear');
};
script>
父组件完整使用示例:
<template>
<div class="app-container">
<ArticleCard
:title="article.title"
:description="article.desc"
:views="article.views"
@read-more="handleReadMore"
/>
<SearchBox
@search="handleSearch"
@clear="searchText = ''"
/>
<p>当前搜索: {{ searchText }}p>
div>
template>
<script setup>
import { ref } from 'vue';
import ArticleCard from './ArticleCard.vue';
import SearchBox from './SearchBox.vue';
const article = ref({
title: 'Vue 3组件通信指南',
desc: '详细介绍各种组件通信方式',
views: 1024
});
const searchText = ref('');
const handleReadMore = (articleId) => {
console.log('阅读更多:', articleId);
// 导航到详情页...
};
const handleSearch = (keyword) => {
searchText.value = keyword;
// 执行搜索逻辑...
};
script>
最佳实践提示:
provide
和inject
是Vue提供的一对API,用于实现组件树的跨层级通信,特别适合解决"prop逐层透传"的问题,让数据可以在祖先组件和后代组件之间直接传递,而不需要经过中间每一层的组件。
provide
函数,可以提供一个键值对,键是一个字符串标识符,值是要传递的数据。inject
函数,通过相同的键名来获取祖先组件提供的数据。在祖先组件中使用provide
提供数据:
<template>
<div>
<Children />
div>
template>
<script setup>
import { provide } from 'vue';
import Children from './Children.vue';
// 提供静态数据
provide('globalData', '这是全局数据');
// 也可以提供响应式数据
const count = ref(0);
provide('countData', count);
script>
在后代组件中通过inject
获取数据:
<template>
<div>
<p>注入的数据: {{ globalData }}p>
<p>注入的响应式数据: {{ countData }}p>
<button @click="countData++">增加计数button>
div>
template>
<script setup>
import { inject } from 'vue';
// 注入静态数据
const globalData = inject('globalData');
// 注入响应式数据
const countData = inject('countData');
script>
const value = inject('someKey', '默认值');
const value = inject('someKey', () => new ExpensiveClass());
provide('readOnlyData', readonly(someData));
在Vue.js应用开发中,随着项目规模扩大,组件间的状态共享和通信会变得复杂。这时候就需要使用状态管理工具来集中管理应用状态。Vuex是Vue的官方状态管理库,而Pinia则是最新的推荐解决方案,具有更简洁的API和TypeScript支持。
Pinia的基本使用分为三个步骤:
import { defineStore } from 'pinia';
// 使用defineStore定义store
// 第一个参数是store的唯一ID
export const useCounterStore = defineStore('counter', {
// state使用函数返回初始状态
state: () => ({
count: 0,
title: 'My Counter'
}),
// actions定义业务逻辑
actions: {
increment() {
this.count++; // 直接修改state
},
async fetchData() {
// 可以包含异步操作
const response = await fetch('/api/data');
// ...
}
},
// getters相当于计算属性
getters: {
doubleCount: (state) => state.count * 2
}
});
<template>
<div>
<h2>{{ counterStore.title }}h2>
<p>当前计数: {{ counterStore.count }}p>
<p>双倍计数: {{ counterStore.doubleCount }}p>
<button @click="counterStore.increment">增加button>
<button @click="resetCounter">重置button>
div>
template>
<script setup>
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
// 使用store
const counterStore = useCounterStore();
// 如果需要解构,使用storeToRefs保持响应性
const { title } = storeToRefs(counterStore);
// 可以直接调用action
function resetCounter() {
counterStore.$reset(); // 重置state
}
script>
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null,
token: ''
}),
actions: {
login(userData) {
this.userInfo = userData;
this.token = 'generated_token';
localStorage.setItem('token', this.token);
},
logout() {
this.userInfo = null;
this.token = '';
localStorage.removeItem('token');
}
}
});
// stores/cart.js
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
actions: {
addItem(product) {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity++;
} else {
this.items.push({...product, quantity: 1});
}
this.calculateTotal();
},
calculateTotal() {
this.total = this.items.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
);
}
}
});
Pinia的这些特性使它成为Vue 3应用中管理状态的首选方案,特别是在需要处理复杂状态逻辑、跨组件共享数据或需要良好TypeScript支持的项目中。
对于非父子组件之间的复杂通信场景(如跨多级组件、兄弟组件等),可以引入轻量级的第三方事件总线库如mitt或tiny-emitter。这种方法通过发布订阅模式实现解耦通信。下面以mitt为例详细介绍:
首先通过npm安装mitt库:
npm install mitt
# 或者使用yarn
yarn add mitt
创建独立的eventBus.js文件作为事件中心:
// src/utils/eventBus.js
import mitt from 'mitt';
// 创建mitt实例
const emitter = mitt();
// 可选:定义全局事件类型常量
export const EventTypes = {
CUSTOM_EVENT: 'customEvent',
USER_LOGIN: 'userLogin'
};
export default emitter;
在组件A中发布事件,可传递任意数据:
<template>
<button @click="sendEvent">发送全局事件button>
<button @click="sendUserInfo">发送用户信息button>
template>
<script setup>
import emitter, { EventTypes } from '@/utils/eventBus';
const sendEvent = () => {
// 触发普通事件
emitter.emit(EventTypes.CUSTOM_EVENT, {
timestamp: new Date(),
message: '来自组件A的重要通知'
});
};
const sendUserInfo = () => {
// 触发带用户数据的事件
emitter.emit(EventTypes.USER_LOGIN, {
userId: 'U123456',
username: '张三'
});
};
script>
在组件B中订阅事件:
<template>
<div>
<p>收到消息: {{ message }}p>
<p>用户状态: {{ userStatus }}p>
div>
template>
<script setup>
import { ref, onUnmounted } from 'vue';
import emitter, { EventTypes } from '@/utils/eventBus';
const message = ref('');
const userStatus = ref('未登录');
// 监听自定义事件
const eventHandler = (data) => {
message.value = `${data.message} (${new Date(data.timestamp).toLocaleTimeString()})`;
};
// 监听用户登录事件
const loginHandler = (user) => {
userStatus.value = `${user.username}已登录(ID:${user.userId})`;
};
// 组件挂载时注册监听
emitter.on(EventTypes.CUSTOM_EVENT, eventHandler);
emitter.on(EventTypes.USER_LOGIN, loginHandler);
// 组件卸载时移除监听
onUnmounted(() => {
emitter.off(EventTypes.CUSTOM_EVENT, eventHandler);
emitter.off(EventTypes.USER_LOGIN, loginHandler);
});
script>
emitter.once('one-time-event', () => {
console.log('只会触发一次');
});
emitter.all.clear();
type Events = {
search: string
change: number
};
const emitter = mitt<Events>();
emitter.emit('search', 'query'); // OK
emitter.emit('change', 123); // OK
相比Vue2的EventBus,mitt更轻量(200b),且不依赖Vue实例,适合简单的跨组件通信场景。
data
、methods
、computed
等选项,统一在一个函数内进行组件逻辑的组织。主要功能包括:
特性说明:
this
上下文,所有操作都通过导入的Vue API实现典型应用场景:
示例扩展:
<template>
<div>
<p>{{ count }}p>
<button @click="increment">+1button>
<p>{{ doubledCount }}p>
div>
template>
<script setup>
import { ref, computed } from 'vue';
// 响应式数据
const count = ref(0);
// 计算方法
const doubledCount = computed(() => count.value * 2);
// 组件方法
function increment() {
count.value++;
}
// 暴露给模板
defineExpose({
count,
increment
})
script>
注意事项:
语法糖中,所有顶层绑定自动暴露给模板defineExpose
onMounted
)在setup内注册在组件即将挂载到DOM之前调用,此阶段具有以下特点:
$el
属性尚未生成,无法访问DOM元素典型应用场景:
在组件挂载到DOM之后调用,此阶段具有以下特点:
实际开发中的典型用法示例:
<template>
<div id="app">
<canvas ref="chartCanvas">canvas>
div>
template>
<script setup>
import { onMounted, ref } from 'vue';
import Chart from 'chart.js';
const chartCanvas = ref(null);
onMounted(() => {
// 初始化图表
new Chart(chartCanvas.value, {
type: 'bar',
data: {/*...*/},
options: {/*...*/}
});
// 获取DOM元素尺寸
const dimensions = {
width: chartCanvas.value.offsetWidth,
height: chartCanvas.value.offsetHeight
};
// 添加事件监听
window.addEventListener('resize', handleResize);
});
script>
注意事项:
onBeforeMount
中不要尝试访问DOM,因为此时DOM还不存在onMounted
不会在服务器端执行onUnmounted
生命周期钩子中进行组件更新阶段是Vue响应式系统中重要的生命周期环节,当组件依赖的响应式数据发生变化时,会触发更新流程。这一阶段主要包含两个关键钩子函数:
onBeforeUpdate:在组件数据更新之前调用。此时Vue已经检测到数据变化并准备更新DOM,但DOM尚未实际更新。这个钩子常用于获取更新前的DOM状态或执行更新前的准备工作。
典型应用场景:
onUpdated:在组件数据更新之后调用,此时DOM已经根据更新后的数据完成了重新渲染。这个钩子适合执行依赖新DOM的操作,但要注意避免在此修改响应式数据,否则可能导致无限更新循环。
常见使用场景:
<template>
<div>
<p>当前计数:{{ count }}p>
<button @click="increment">增加计数button>
<div ref="messageBox" style="height:100px;overflow:auto;border:1px solid #ccc;margin-top:10px">
<p v-for="msg in messages" :key="msg">{{ msg }}p>
div>
div>
template>
<script setup>
import { ref, onBeforeUpdate, onUpdated } from 'vue';
const count = ref(0);
const messages = ref(['初始消息']);
const messageBox = ref(null);
// 记录更新前的滚动位置
let prevScrollHeight = 0;
const increment = () => {
count.value++;
messages.value.push(`新消息 ${count.value}`);
};
onBeforeUpdate(() => {
console.log('[BeforeUpdate] 组件即将更新');
if (messageBox.value) {
prevScrollHeight = messageBox.value.scrollHeight;
}
});
onUpdated(() => {
console.log('[Updated] 组件已完成更新');
// 保持滚动位置不变
if (messageBox.value) {
messageBox.value.scrollTop = messageBox.value.scrollHeight - prevScrollHeight;
}
// 更新后自动聚焦到按钮
document.querySelector('button')?.focus();
});
script>
示例说明:
注意事项:
onBeforeUnmount:在组件即将卸载之前调用,主要用于执行清理工作。这是最后的机会来处理组件相关的资源释放,常见应用场景包括:
onUnmounted:在组件完全卸载之后调用,此时组件实例及其所有子组件都已被销毁。通常用于:
<template>
<div v-if="show">
<p>这是一个组件p>
<div id="chart-container">div>
div>
<button @click="hideComponent">隐藏组件button>
template>
<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue';
import * as echarts from 'echarts'; // 引入Echarts库
const show = ref(true);
const chartInstance = ref(null); // 存储图表实例
const hideComponent = () => {
show.value = false;
};
// 模拟一个定时器
let timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 模拟一个事件监听
const handleResize = () => console.log('窗口大小改变');
window.addEventListener('resize', handleResize);
// 初始化图表
const initChart = () => {
chartInstance.value = echarts.init(document.getElementById('chart-container'));
chartInstance.value.setOption({/* 图表配置 */});
};
initChart();
onBeforeUnmount(() => {
// 清理定时器
clearInterval(timer);
console.log('定时器已清除');
// 移除事件监听
window.removeEventListener('resize', handleResize);
console.log('事件监听已移除');
// 销毁图表实例
if(chartInstance.value) {
chartInstance.value.dispose();
console.log('图表实例已销毁');
}
console.log('组件即将卸载,资源清理完成');
});
onUnmounted(() => {
console.log('组件已完全卸载');
// 可以在这里发送组件卸载的埋点数据
// analytics.track('ComponentUnmounted');
});
script>
当组件树中的任意后代组件抛出错误时,该钩子会被触发。它是 Vue 3 中用于构建组件级错误边界的重要机制。
核心功能:
典型应用场景:
参数详解:
onErrorCaptured((error, instance, info) => {
// error: 错误对象
// instance: 触发错误的组件实例
// info: 错误来源信息字符串(如:'render function')
})
示例扩展:
<template>
<div>
<ErrorBoundary>
<ChildComponent />
ErrorBoundary>
<div v-if="error">
组件加载失败,请<a @click="retry">重试a>
div>
div>
template>
<script setup>
import { ref } from 'vue';
const error = ref(null);
const retry = () => location.reload();
onErrorCaptured((err) => {
error.value = err;
// 阻止错误继续冒泡
return false;
// 如需继续传播则返回true
});
script>
最佳实践建议:
错误传播控制:
通过返回布尔值决定是否阻止错误继续冒泡:
return false
:阻止传播return true
:允许继续传播return true
调试技巧:
在开发环境中,可以利用该钩子快速定位组件问题:
onErrorCaptured((err, vm, info) => {
console.group('[ErrorCaptured]');
console.log('Component:', vm.type.__name);
console.log('Info:', info);
console.error(err);
console.groupEnd();
});
Vue3的组件通信与生命周期机制为开发者提供了丰富且灵活的工具。通过合理运用各种通信方式,结合组件生命周期钩子函数,能够构建出结构清晰、交互流畅的前端应用,满足不同业务场景的需求。在实际开发过程中,开发者应根据项目的具体情况,选择最合适的通信和生命周期处理方式,提升开发效率与应用质量。
下期预告:Vue Router与Vuex核心应用
❤️❤️❤️:如果你觉得这篇文章对你有帮助,欢迎点赞、关注本专栏!后续解锁更多功能,敬请期待!
更多专栏汇总:
前端面试专栏
Node.js 实训专栏