生命周期钩子是Vue提供的一系列函数,让开发者可以在组件不同阶段添加自定义代码。比如组件创建时、挂载到DOM时、数据更新时或组件销毁时执行特定操作。
Vue组件从创建到销毁的整个过程称为生命周期。在此过程中,Vue实例会经历一系列的初始化步骤——例如设置数据监听、编译模板、挂载实例到DOM、在数据变化时更新DOM等。
钩子函数就是在特定时间点自动执行的函数。Vue为开发者提供了多个钩子函数,允许我们在特定阶段运行自己的代码。
Vue的生命周期可以分为四个主要阶段:创建、挂载、更新和销毁。
┌─────────────────────┐
│ 创建阶段 │
│ beforeCreate │ ← 实例创建前,data和methods都不可用
│ created │ ← 实例创建后,data和methods可用,但DOM未挂载
└─────────────────────┘
↓
┌─────────────────────┐
│ 挂载阶段 │
│ beforeMount │ ← DOM挂载前,template已编译但未渲染到页面
│ mounted │ ← DOM挂载后,可访问DOM元素
└─────────────────────┘
↓
┌─────────────────────┐
│ 更新阶段 │
│ beforeUpdate │ ← 数据更新前,DOM更新前
│ updated │ ← 数据更新后,DOM更新后
└─────────────────────┘
↓
┌─────────────────────┐
│ 销毁阶段 │
│ beforeUnmount │ ← 组件卸载前(Vue 3)/beforeDestroy(Vue 2)
│ unmounted │ ← 组件卸载后(Vue 3)/destroyed(Vue 2)
└─────────────────────┘
在实例初始化之后,数据观测(data observer)和事件配置(event/watcher)之前被调用。
特点:
示例:
export default {
beforeCreate() {
console.log('beforeCreate钩子执行了');
console.log('能否访问data:', this.message === undefined);
console.log('能否访问methods:', this.sayHello === undefined);
},
data() {
return {
message: 'Hello Vue!'
}
},
methods: {
sayHello() {
console.log(this.message);
}
}
}
适用场景:
在实例创建完成后被立即调用。此时已完成数据观测、计算属性、方法、事件/侦听器的配置。
特点:
示例:
export default {
data() {
return {
users: [],
loading: true
}
},
created() {
console.log('created钩子执行了');
console.log('能否访问data:', this.loading);
// 在组件创建后立即获取数据
this.fetchUsers();
},
methods: {
async fetchUsers() {
try {
const response = await fetch('https://api.example.com/users');
this.users = await response.json();
} catch (error) {
console.error('获取用户数据失败:', error);
} finally {
this.loading = false;
}
}
}
}
适用场景:
在挂载开始之前被调用:相关的render函数首次被调用。
特点:
示例:
export default {
data() {
return {
message: 'Hello Vue!'
}
},
beforeMount() {
console.log('beforeMount钩子执行了');
console.log('DOM元素是否可访问:', document.getElementById('app') === null);
console.log('当前数据:', this.message);
}
}
适用场景:
在实例挂载到DOM后调用,此时可以访问DOM元素。
特点:
示例:
export default {
data() {
return {
chartData: [30, 50, 20, 40, 90]
}
},
mounted() {
console.log('mounted钩子执行了');
console.log('DOM元素可访问:', document.getElementById('chart') !== null);
// 初始化图表库
this.initChart();
// 添加窗口调整事件监听
window.addEventListener('resize', this.handleResize);
},
methods: {
initChart() {
const chartEl = document.getElementById('chart');
if (!chartEl) return;
// 使用第三方库初始化图表
const myChart = new Chart(chartEl, {
type: 'bar',
data: {
datasets: [{
data: this.chartData
}]
}
});
},
handleResize() {
// 窗口大小变化时重新调整图表大小
console.log('窗口大小已改变,调整图表尺寸');
}
},
beforeUnmount() {
// 移除事件监听器,避免内存泄漏
window.removeEventListener('resize', this.handleResize);
}
}
适用场景:
数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
特点:
示例:
export default {
data() {
return {
count: 0,
lastCount: 0
}
},
methods: {
increment() {
this.count++;
}
},
beforeUpdate() {
console.log('beforeUpdate钩子执行了');
// 记录更新前的DOM中显示的值
const domValue = parseInt(document.getElementById('counter').textContent);
console.log('DOM中的值:', domValue);
console.log('data中的值:', this.count);
// 记录上一次的值
this.lastCount = domValue;
}
}
适用场景:
由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。
特点:
示例:
export default {
data() {
return {
items: [],
isLoading: false
}
},
methods: {
async loadMoreItems() {
this.isLoading = true;
const response = await fetch('https://api.example.com/items');
const newItems = await response.json();
this.items = [...this.items, ...newItems];
this.isLoading = false;
}
},
updated() {
console.log('updated钩子执行了');
// 如果新内容被加载,滚动到底部
if (this.items.length && !this.isLoading) {
const container = this.$refs.itemsContainer;
if (container) {
// 滚动到新加载的内容
container.scrollTop = container.scrollHeight;
}
}
// 注意:避免在此处修改会触发更新的数据
// 错误示范: this.items = [...]; // 会导致无限循环!
}
}
适用场景:
在卸载组件实例之前调用。在这个阶段,实例仍然完全可用。
特点:
示例:
export default {
data() {
return {
intervalId: null,
fetchController: null
}
},
created() {
// 设置定时更新
this.intervalId = setInterval(() => {
console.log('定时更新');
this.updateData();
}, 3000);
// 设置可取消的网络请求
this.fetchController = new AbortController();
},
methods: {
async updateData() {
try {
const response = await fetch('https://api.example.com/data', {
signal: this.fetchController.signal
});
const data = await response.json();
this.processData(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('数据更新失败:', error);
}
}
},
processData(data) {
// 处理获取的数据
}
},
beforeUnmount() { // Vue 3
// beforeDestroy() { // Vue 2
console.log('beforeUnmount钩子执行了');
// 清除定时器
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
// 取消进行中的网络请求
if (this.fetchController) {
this.fetchController.abort();
this.fetchController = null;
}
// 移除所有可能的事件监听器
window.removeEventListener('resize', this.handleResize);
}
}
适用场景:
卸载组件实例后调用。调用此钩子时,组件实例的所有指令已被解绑,所有事件监听器已被移除。
特点:
示例:
export default {
unmounted() { // Vue 3
// destroyed() { // Vue 2
console.log('unmounted钩子执行了');
console.log('组件已完全销毁');
// 可以通知父组件或其他系统该组件已销毁
this.$emit('component-unmounted', this.componentId);
// 可以执行一些额外的清理
localStorage.removeItem(`cache_${this.componentId}`);
}
}
适用场景:
这两个钩子函数只在使用了
组件包裹的动态组件中可用。
activated:被 keep-alive 缓存的组件激活时调用。
deactivated:被 keep-alive 缓存的组件停用时调用。
示例:
export default {
data() {
return {
loadTime: null,
timeSinceActivation: 0,
timer: null
}
},
activated() {
console.log('activated钩子执行了');
// 记录组件被激活的时间
this.loadTime = new Date();
// 开始计时,记录组件被激活后的停留时间
this.timer = setInterval(() => {
this.timeSinceActivation = Math.floor(
(new Date() - this.loadTime) / 1000
);
}, 1000);
// 组件激活时重新获取最新数据
this.fetchLatestData();
},
deactivated() {
console.log('deactivated钩子执行了');
// 记录用户在此组件停留的总时间
const stayTime = Math.floor((new Date() - this.loadTime) / 1000);
console.log(`用户在组件停留了 ${stayTime} 秒`);
// 清除计时器
clearInterval(this.timer);
// 可以保存一些状态,以便下次激活时恢复
localStorage.setItem('scrollPosition', window.scrollY);
},
methods: {
fetchLatestData() {
// 获取最新数据的逻辑
}
}
}
适用场景:
当捕获一个来自子孙组件的错误时被调用。
特点:
示例:
export default {
errorCaptured(error, instance, info) {
console.log('errorCaptured钩子捕获到错误');
console.error('错误信息:', error);
console.log('错误组件实例:', instance);
console.log('错误来源信息:', info);
// 记录错误
this.logError(error, info);
// 显示用户友好的错误信息
this.showErrorNotification('抱歉,出现了一个错误,我们正在处理');
// 返回false阻止错误继续向上传播
return false;
},
methods: {
logError(error, info) {
// 将错误信息发送到服务器日志系统
},
showErrorNotification(message) {
// 显示用户友好的错误消息
this.errorMessage = message;
this.showErrorModal = true;
}
}
}
适用场景:
这两个钩子用于调试。它们仅在开发模式下生效,仅用于开发阶段。
renderTracked:跟踪虚拟DOM重新渲染时触发。接收包含触发渲染的依赖的信息。
renderTriggered:当虚拟DOM重新渲染被触发时调用。接收包含触发重新渲染的依赖的信息。
示例:
export default {
renderTracked(event) {
console.log('renderTracked钩子执行了');
console.log('跟踪的渲染依赖:', event);
},
renderTriggered(event) {
console.log('renderTriggered钩子执行了');
console.log('触发重新渲染的依赖:', event);
console.log(`由 ${event.target.__proto__.constructor.name}.${event.key} 触发`);
}
}
适用场景:
Vue 3 中一些生命周期钩子名称发生了变化,同时也提供了Composition API的等效钩子函数。
Vue 2 选项式API | Vue 3 选项式API | Vue 3 Composition API |
---|---|---|
beforeCreate | beforeCreate | setup() |
created | created | setup() |
beforeMount | beforeMount | onBeforeMount |
mounted | mounted | onMounted |
beforeUpdate | beforeUpdate | onBeforeUpdate |
updated | updated | onUpdated |
beforeDestroy | beforeUnmount | onBeforeUnmount |
destroyed | unmounted | onUnmounted |
activated | activated | onActivated |
deactivated | deactivated | onDeactivated |
errorCaptured | errorCaptured | onErrorCaptured |
- | renderTracked | onRenderTracked |
- | renderTriggered | onRenderTriggered |
- | serverPrefetch | onServerPrefetch |
选项式API vs Composition API示例:
// 选项式API (Vue 2 和 Vue 3)
export default {
data() {
return {
message: 'Hello'
}
},
mounted() {
console.log('组件已挂载');
console.log(this.message);
},
updated() {
console.log('组件已更新');
},
beforeUnmount() {
console.log('组件即将卸载');
}
}
// Composition API (Vue 3)
import { ref, onMounted, onUpdated, onBeforeUnmount } from 'vue';
export default {
setup() {
const message = ref('Hello');
onMounted(() => {
console.log('组件已挂载');
console.log(message.value);
});
onUpdated(() => {
console.log('组件已更新');
});
onBeforeUnmount(() => {
console.log('组件即将卸载');
});
return {
message
}
}
}
在正确的钩子中执行操作
created
mounted
beforeUnmount
避免在不必要的钩子中执行昂贵操作
updated
钩子中执行复杂计算updated
钩子中的逻辑注意钩子中的异步操作
this.$nextTick()
或 await nextTick()
等待DOM更新完成组件间通信
mounted
调用对子组件的操作$refs
访问子组件实例问题:父子组件的生命周期钩子执行顺序可能令人困惑。
解决:了解执行顺序:父组件beforeCreate→父created→父beforeMount→子beforeCreate→子created→子beforeMount→子mounted→父mounted
父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted
问题:在 updated
钩子中再次修改数据会触发新的更新周期,导致无限循环。
解决:在 updated
中添加条件检查,或使用计算属性和侦听器代替。
// 错误示例
updated() {
this.count++; // 会导致无限循环!
}
// 正确示例
updated() {
if (!this.hasUpdated) {
this.hasUpdated = true;
this.finalizeUpdate();
}
}
问题:在 created
中尝试访问DOM元素报错。
解决:将DOM操作移到 mounted
钩子中,或使用 $nextTick
。
created() {
// 错误: document.getElementById('myElement') 可能为 null
// 正确: 使用 $nextTick
this.$nextTick(() => {
// DOM 现在已更新
const element = document.getElementById('myElement');
});
}
问题:异步操作完成后组件已销毁,但仍尝试更新数据导致警告。
解决:跟踪组件是否已销毁,或在 beforeUnmount
中取消异步操作。
export default {
data() {
return {
isDestroyed: false,
fetchController: new AbortController()
}
},
methods: {
async fetchData() {
try {
const response = await fetch('/api/data', {
signal: this.fetchController.signal
});
const data = await response.json();
// 检查组件是否已销毁
if (!this.isDestroyed) {
this.data = data;
}
} catch (error) {
if (error.name !== 'AbortError') {
console.error(error);
}
}
}
},
beforeUnmount() {
this.isDestroyed = true;
this.fetchController.abort();
}
}
下面是一个用户资料编辑组件的综合案例,展示了不同生命周期钩子的使用:
export default {
name: 'UserProfileEditor',
props: {
userId: {
type: String,
required: true
}
},
data() {
return {
user: null,
originalUserData: null,
isLoading: true,
isSaving: false,
error: null,
saveTimer: null,
formChanged: false,
unsavedChanges: false,
imageUploader: null
}
},
// 创建阶段:准备数据
beforeCreate() {
console.log('1. beforeCreate - 组件初始化');
},
created() {
console.log('2. created - 加载用户数据');
// 获取用户数据
this.fetchUserData();
// 添加页面离开提示
window.addEventListener('beforeunload', this.handlePageLeave);
},
// 挂载阶段:操作DOM
beforeMount() {
console.log('3. beforeMount - 准备渲染DOM');
},
mounted() {
console.log('4. mounted - DOM已渲染,初始化编辑器');
// 初始化第三方图片上传组件
this.initImageUploader();
// 设置自动保存定时器
this.saveTimer = setInterval(() => {
if (this.formChanged && !this.isSaving) {
this.autoSave();
this.formChanged = false;
}
}, 30000); // 每30秒自动保存一次
},
// 更新阶段:响应数据变化
beforeUpdate() {
console.log('5. beforeUpdate - 数据已更新,DOM即将重新渲染');
},
updated() {
console.log('6. updated - DOM已重新渲染');
// 如果表单数据与原始数据不同,标记为已更改
if (this.user && this.originalUserData) {
const currentData = JSON.stringify(this.user);
const originalData = JSON.stringify(this.originalUserData);
if (currentData !== originalData) {
this.unsavedChanges = true;
this.formChanged = true;
} else {
this.unsavedChanges = false;
}
}
},
// 销毁阶段:清理资源
beforeUnmount() {
console.log('7. beforeUnmount - 组件即将销毁,清理资源');
// 提示保存未保存的更改
if (this.unsavedChanges) {
this.saveChanges();
}
// 清除定时器
clearInterval(this.saveTimer);
// 销毁图片上传组件
if (this.imageUploader) {
this.imageUploader.destroy();
}
// 移除事件监听器
window.removeEventListener('beforeunload', this.handlePageLeave);
},
unmounted() {
console.log('8. unmounted - 组件已销毁');
},
// 特殊钩子:错误捕获
errorCaptured(error, vm, info) {
console.error('错误捕获:', error, info);
this.error = `发生错误: ${error.message}`;
// 记录错误
this.logError(error, info);
// 防止错误向上传播
return false;
},
methods: {
async fetchUserData() {
try {
this.isLoading = true;
this.error = null;
const response = await fetch(`/api/users/${this.userId}`);
if (!response.ok) throw new Error('获取用户数据失败');
this.user = await response.json();
// 保存原始数据副本用于比较
this.originalUserData = JSON.parse(JSON.stringify(this.user));
} catch (error) {
this.error = error.message;
console.error('获取用户数据错误:', error);
} finally {
this.isLoading = false;
}
},
initImageUploader() {
const uploadElement = this.$refs.imageUploader;
if (!uploadElement) return;
// 初始化假设的图片上传库
this.imageUploader = new ImageUploader(uploadElement, {
maxSize: 5 * 1024 * 1024, // 5MB
onUpload: this.handleImageUpload
});
},
async saveChanges() {
if (!this.user) return;
try {
this.isSaving = true;
const response = await fetch(`/api/users/${this.userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.user)
});
if (!response.ok) throw new Error('保存数据失败');
// 更新原始数据
this.originalUserData = JSON.parse(JSON.stringify(this.user));
this.unsavedChanges = false;
// 显示成功提示
this.showMessage('保存成功');
} catch (error) {
this.error = error.message;
console.error('保存用户数据错误:', error);
this.showMessage('保存失败: ' + error.message, 'error');
} finally {
this.isSaving = false;
}
},
autoSave() {
console.log('执行自动保存...');
this.saveChanges();
},
handleImageUpload(result) {
if (result.success) {
this.user.avatarUrl = result.url;
} else {
this.error = '图片上传失败: ' + result.error;
}
},
handlePageLeave(event) {
if (this.unsavedChanges) {
const message = '有未保存的更改,确定要离开吗?';
event.returnValue = message;
return message;
}
},
logError(error, info) {
// 发送错误到日志服务器
fetch('/api/log-error', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
error: error.message,
stack: error.stack,
info,
component: 'UserProfileEditor',
userId: this.userId,
timestamp: new Date().toISOString()
})
}).catch(e => console.error('无法记录错误:', e));
},
showMessage(text, type = 'success') {
// 显示消息提示
console.log(`[${type}] ${text}`);
// 实际应用中可能使用UI组件库的消息提示功能
}
}
}
Vue的生命周期钩子提供了完善的机制,允许开发者在组件不同阶段执行代码,从而实现各种复杂的功能和优化。
生命周期阶段:
beforeCreate
、created
beforeMount
、mounted
beforeUpdate
、updated
beforeUnmount
、unmounted
activated
、deactivated
、errorCaptured
等常见使用场景:
created
mounted
updated
beforeUnmount
Vue 2与Vue 3的区别:
最佳实践:
深入理解Vue的生命周期钩子,将帮助你更有效地组织代码逻辑,提高应用性能,并构建更加健壮的Vue应用。