Vue 生命周期是每个 Vue 开发者必须掌握的核心概念。简单来说,它描述了 Vue 组件从创建到销毁的完整过程。在这个过程中,Vue 提供了一系列的"钩子函数",让我们可以在特定阶段插入自己的逻辑代码。
理解生命周期的重要性体现在几个方面:
Vue 3 在生命周期方面做了一些调整,主要是为了更准确地反映组件的实际状态:
| Vue 2 钩子 | Vue 3 钩子 | 变化说明 |
|---|---|---|
| beforeCreate | beforeCreate | 保持不变 |
| created | created | 保持不变 |
| beforeMount | beforeMount | 保持不变 |
| mounted | mounted | 保持不变 |
| beforeUpdate | beforeUpdate | 保持不变 |
| updated | updated | 保持不变 |
| beforeDestroy | beforeUnmount | 更名,语义更准确 |
| destroyed | unmounted | 更名,语义更准确 |
Vue 3 将"destroy"改为"unmount"主要是为了:
javascript复制export default {
beforeCreate() {
console.log('beforeCreate 钩子被调用');
// 此时无法访问data和methods
console.log(this.message); // undefined
console.log(this.sayHello); // undefined
},
data() {
return {
message: 'Hello Vue'
}
},
methods: {
sayHello() {
console.log(this.message);
}
}
}
关键点:
实际应用场景:
javascript复制export default {
data() {
return {
posts: [],
loading: false
}
},
created() {
console.log('created 钩子被调用');
// 可以访问data和methods了
console.log(this.message); // 'Hello Vue'
this.sayHello(); // 可以调用方法
// 常见的使用场景:发起API请求
this.fetchPosts();
},
methods: {
async fetchPosts() {
this.loading = true;
try {
const response = await fetch('/api/posts');
this.posts = await response.json();
} catch (error) {
console.error('获取数据失败:', error);
} finally {
this.loading = false;
}
}
}
}
关键点:
最佳实践:
javascript复制export default {
beforeMount() {
console.log('beforeMount 钩子被调用');
// 模板已编译但还未挂载
console.log(this.$el); // undefined
// $refs还不存在
console.log(this.$refs.myElement); // undefined
}
}
关键点:
javascript复制export default {
mounted() {
console.log('mounted 钩子被调用');
// 可以访问DOM了
console.log(this.$el); // 真实的DOM元素
console.log(this.$refs.myElement); // 引用的DOM元素
// 初始化第三方库
this.initChart();
// 添加事件监听
window.addEventListener('resize', this.handleResize);
},
methods: {
initChart() {
this.chart = echarts.init(this.$refs.chartContainer);
this.chart.setOption({
// 图表配置
});
},
handleResize() {
this.chart.resize();
}
}
}
关键点:
常见用途:
javascript复制export default {
data() {
return {
count: 0
}
},
beforeUpdate() {
console.log('beforeUpdate 钩子被调用');
// 可以获取更新前的DOM状态
const oldValue = this.$refs.counter.textContent;
console.log('旧值:', oldValue);
// 危险操作:可能导致无限循环
// this.count++; // 不要这样做!
}
}
关键点:
使用场景:
javascript复制export default {
data() {
return {
items: []
}
},
updated() {
console.log('updated 钩子被调用');
// DOM已经更新
console.log('新值:', this.$refs.list.innerHTML);
// 可以基于新DOM进行操作
this.adjustLayout();
},
methods: {
adjustLayout() {
// 根据新的DOM状态调整布局
}
}
}
关键点:
注意事项:
javascript复制export default {
data() {
return {
timer: null,
socket: null
}
},
mounted() {
this.timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
this.socket = new WebSocket('ws://example.com');
this.socket.onmessage = (event) => {
console.log('收到消息:', event.data);
};
},
beforeUnmount() {
console.log('beforeUnmount 钩子被调用');
// 清除定时器
clearInterval(this.timer);
this.timer = null;
// 关闭WebSocket连接
if (this.socket) {
this.socket.close();
this.socket = null;
}
// 移除事件监听
window.removeEventListener('resize', this.handleResize);
// 取消未完成的请求
if (this.controller) {
this.controller.abort();
}
}
}
关键点:
必须清理的资源:
javascript复制export default {
unmounted() {
console.log('unmounted 钩子被调用');
// 理论上不应该再访问DOM
// 但某些情况下可能还能访问到
// 最后的清理工作
this.cleanup();
},
methods: {
cleanup() {
// 执行最终的清理
}
}
}
关键点:
Vue 3 的 Composition API 提供了新的方式来使用生命周期钩子:
javascript复制<script setup>
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
// setup 相当于 beforeCreate + created
const count = ref(0);
onBeforeMount(() => {
console.log('onBeforeMount');
});
onMounted(() => {
console.log('onMounted');
// 初始化图表
const chart = echarts.init(document.getElementById('chart'));
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
});
onUpdated(() => {
console.log('onUpdated');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
// 清理工作
});
onUnmounted(() => {
console.log('onUnmounted');
});
</script>
对比表:
| 选项式 API | Composition API |
|---|---|
| beforeCreate | setup() |
| created | setup() |
| beforeMount | onBeforeMount |
| mounted | onMounted |
| beforeUpdate | onBeforeUpdate |
| updated | onUpdated |
| beforeUnmount | onBeforeUnmount |
| unmounted | onUnmounted |
优势:
javascript复制export default {
data() {
return {
user: null,
posts: [],
loading: false,
error: null
}
},
async created() {
// 并行请求
await Promise.all([
this.fetchUser(),
this.fetchPosts()
]);
},
methods: {
async fetchUser() {
this.loading = true;
try {
const response = await fetch('/api/user');
this.user = await response.json();
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
async fetchPosts() {
try {
const response = await fetch('/api/posts');
this.posts = await response.json();
} catch (err) {
console.error('获取帖子失败:', err);
}
}
}
}
最佳实践:
javascript复制export default {
data() {
return {
map: null
}
},
mounted() {
this.initMap();
},
methods: {
initMap() {
// 初始化地图
this.map = new AMap.Map('map-container', {
zoom: 13,
center: [116.397428, 39.90923]
});
// 添加控件
this.map.addControl(new AMap.ToolBar());
// 添加事件
this.map.on('click', (e) => {
console.log('点击位置:', e.lnglat);
});
}
},
beforeUnmount() {
// 销毁地图实例
if (this.map) {
this.map.destroy();
this.map = null;
}
}
}
关键点:
javascript复制export default {
data() {
return {
largeList: [],
visibleItems: 50
}
},
created() {
// 初始只加载部分数据
this.fetchPartialData();
},
mounted() {
// 滚动加载更多
window.addEventListener('scroll', this.handleScroll);
},
methods: {
async fetchPartialData() {
const response = await fetch('/api/large-list');
this.largeList = await response.json();
},
handleScroll() {
const nearBottom = window.innerHeight + window.scrollY >=
document.body.offsetHeight - 500;
if (nearBottom && this.visibleItems < this.largeList.length) {
this.visibleItems += 20;
}
}
},
beforeUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
}
优化策略:
常见泄漏场景:
解决方案:
javascript复制export default {
data() {
return {
timer: null,
socket: null,
controller: null
}
},
mounted() {
// 设置定时器
this.timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
// 建立WebSocket连接
this.socket = new WebSocket('ws://example.com');
// 发起可取消的请求
this.controller = new AbortController();
fetch('/api/data', {
signal: this.controller.signal
});
},
beforeUnmount() {
// 清除定时器
clearInterval(this.timer);
this.timer = null;
// 关闭WebSocket
if (this.socket) {
this.socket.close();
this.socket = null;
}
// 取消请求
if (this.controller) {
this.controller.abort();
this.controller = null;
}
}
}
问题代码:
javascript复制export default {
data() {
return {
count: 0
}
},
updated() {
// 错误:会导致无限循环
this.count++;
}
}
解决方案:
javascript复制export default {
data() {
return {
count: 0,
needsUpdate: false
}
},
watch: {
count(newVal, oldVal) {
if (this.needsUpdate) {
this.doSomething();
this.needsUpdate = false;
}
}
},
methods: {
updateCount() {
this.count++;
this.needsUpdate = true;
},
doSomething() {
// 执行需要的操作
}
}
}
创建阶段顺序:
更新阶段顺序:
销毁阶段顺序:
javascript复制export default {
activated() {
console.log('组件被激活');
// 从缓存中恢复时调用
this.refreshData();
},
deactivated() {
console.log('组件被停用');
// 被缓存时调用
this.saveState();
},
methods: {
refreshData() {
// 刷新数据
},
saveState() {
// 保存状态
}
}
}
使用场景:
javascript复制const routes = [
{
path: '/user/:id',
component: UserDetail,
meta: { keepAlive: true }
}
];
// 父组件
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>
</template>
javascript复制export default {
async created() {
// 可以安全地使用async/await
this.user = await fetchUser();
this.posts = await fetchPosts();
},
async mounted() {
// 等待数据加载后再初始化图表
await this.$nextTick();
this.initChart();
}
}
javascript复制export default {
created() {
console.log('第一个created钩子');
},
created() {
console.log('第二个created钩子');
}
}
// 输出顺序:
// 第一个created钩子
// 第二个created钩子
javascript复制const myMixin = {
created() {
console.log('来自mixin的created钩子');
}
};
export default {
mixins: [myMixin],
created() {
console.log('组件自身的created钩子');
}
}
// 输出顺序:
// 来自mixin的created钩子
// 组件自身的created钩子
javascript复制export default {
errorCaptured(err, vm, info) {
console.error('捕获到错误:', err);
console.log('发生错误的组件:', vm);
console.log('错误信息:', info);
// 可以阻止错误继续向上传播
return false;
},
mounted() {
// 模拟错误
this.undefinedMethod();
}
}
javascript复制<template>
<div v-once>
<!-- 这个div及其子组件只会渲染一次 -->
<StaticComponent />
</div>
</template>
javascript复制const LazyComponent = () => import('./LazyComponent.vue');
export default {
components: {
LazyComponent
}
}
javascript复制export default {
mounted() {
// 延迟执行非关键初始化
setTimeout(() => {
this.initSecondaryFeatures();
}, 0);
}
}
javascript复制import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('调用created钩子时发起请求', () => {
const fetchData = jest.fn();
mount(MyComponent, {
methods: { fetchData }
});
expect(fetchData).toHaveBeenCalled();
});
it('在beforeUnmount中清理资源', async () => {
const wrapper = mount(MyComponent);
const clearResources = jest.spyOn(wrapper.vm, 'clearResources');
await wrapper.unmount();
expect(clearResources).toHaveBeenCalled();
});
});
Vue DevTools提供了生命周期钩子的可视化展示:
javascript复制export default {
created() {
this.$log('created');
},
mounted() {
this.$log('mounted');
},
methods: {
$log(hookName) {
console.log(`[${this.$options.name}] ${hookName} 钩子被调用`);
}
}
}
| Vue 生命周期 | React 生命周期 |
|---|---|
| beforeCreate | constructor |
| created | - |
| beforeMount | componentWillMount |
| mounted | componentDidMount |
| beforeUpdate | componentWillUpdate |
| updated | componentDidUpdate |
| beforeUnmount | componentWillUnmount |
| unmounted | - |
| Vue 生命周期 | Angular 生命周期 |
|---|---|
| beforeCreate | constructor |
| created | ngOnInit |
| beforeMount | - |
| mounted | ngAfterViewInit |
| beforeUpdate | ngDoCheck |
| updated | ngAfterViewChecked |
| beforeUnmount | ngOnDestroy |
| unmounted | - |
typescript复制import { defineComponent } from 'vue';
export default defineComponent({
beforeCreate() {
// 类型安全的钩子
},
created() {
this.fetchData(); // 方法调用有类型检查
},
methods: {
fetchData(): Promise<void> {
return fetch('/api/data').then(res => res.json());
}
}
});
typescript复制<script setup lang="ts">
import { onMounted, ref } from 'vue';
const count = ref(0);
onMounted(() => {
console.log(count.value); // 类型推断为number
});
</script>
javascript复制export default {
created() {
// 初始化store
this.$store.dispatch('initData');
},
mounted() {
// 订阅store变化
this.unsubscribe = this.$store.subscribe((mutation, state) => {
console.log('store变化:', mutation.type);
});
},
beforeUnmount() {
// 取消订阅
this.unsubscribe();
}
}
在SSR中,只有beforeCreate和created会在服务端执行:
javascript复制export default {
created() {
if (process.client) {
// 只在客户端执行的代码
}
}
}
typescript复制import { Vue, Component } from 'vue-property-decorator';
@Component
export default class MyComponent extends Vue {
beforeCreate() {
console.log('beforeCreate');
}
created() {
console.log('created');
}
}
javascript复制export default {
directives: {
focus: {
mounted(el) {
el.focus();
},
updated(el) {
el.focus();
}
}
}
}
javascript复制export default {
beforeEnter(el) {
// 进入过渡前
},
enter(el, done) {
// 进入过渡
done(); // 过渡完成
},
afterEnter(el) {
// 进入过渡后
}
}
javascript复制app.config.errorHandler = (err, vm, info) => {
console.error('全局错误:', err);
console.log('发生错误的组件:', vm);
console.log('错误信息:', info);
};
javascript复制export default {
created() {
this.$perf.start('component-init');
},
mounted() {
this.$perf.end('component-init');
this.$perf.measure('组件初始化', 'component-init');
}
}
javascript复制export default {
beforeCreate() {
// 注册微应用
registerMicroApp({
name: 'my-app',
activeRule: '/my-app',
container: '#app',
props: { /* ... */ }
});
},
beforeUnmount() {
// 卸载微应用
unregisterMicroApp('my-app');
}
}
javascript复制export default {
data() {
return {
worker: null
}
},
created() {
this.worker = new Worker('./worker.js');
this.worker.onmessage = (e) => {
console.log('收到worker消息:', e.data);
};
},
beforeUnmount() {
this.worker.terminate();
}
}
javascript复制export default {
mounted() {
class MyElement extends HTMLElement {
connectedCallback() {
console.log('元素被添加到DOM');
}
disconnectedCallback() {
console.log('元素从DOM移除');
}
}
customElements.define('my-element', MyElement);
}
}
javascript复制export default {
beforeCreate() {
// 此时还没有响应式数据
console.log(this.$data); // undefined
},
created() {
// 数据已变为响应式
console.log(this.$data); // 响应式对象
}
}
javascript复制export default {
render() {
// 在渲染函数中可以访问生命周期状态
return h('div', this.$el ? '已挂载' : '未挂载');
}
}
javascript复制export default {
data() {
return {
currentComponent: 'ComponentA'
}
},
methods: {
toggleComponent() {
this.currentComponent = this.currentComponent === 'ComponentA'
? 'ComponentB'
: 'ComponentA';
}
},
watch: {
currentComponent(newVal, oldVal) {
console.log(`组件从 ${oldVal} 切换到 ${newVal}`);
}
}
}
javascript复制export default {
functional: true,
render(_, { parent }) {
// 函数式组件没有实例,生命周期有限
console.log('函数式组件渲染');
return h('div', '函数式组件');
}
}
javascript复制export default {
mounted() {
console.log('父组件mounted');
},
render() {
return h(Child, null, {
default: () => {
console.log('插槽内容渲染');
return h('div', '插槽内容');
}
});
}
}
javascript复制const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
});
export default {
components: {
AsyncComponent
}
}
javascript复制export default {
mounted() {
console.log('组件已挂载');
},
render() {
return h(Teleport, { to: 'body' }, [
h('div', '被传送的内容')
]);
}
}
javascript复制export default {
components: {
AsyncComponent: defineAsyncComponent(() => import('./AsyncComponent.vue'))
},
setup() {
onMounted(() => {
console.log('父组件mounted');
});
}
}
javascript复制export default {
provide() {
return {
parentData: this.parentData
};
},
data() {
return {
parentData: '父组件数据'
};
},
created() {
console.log('父组件provide数据');
}
}
javascript复制export default {
mounted() {
this.$emit('mounted');
},
beforeUnmount() {
this.$emit('before-unmount');
}
}
javascript复制export default {
props: ['modelValue'],
emits: ['update:modelValue'],
created() {
console.log('初始值:', this.modelValue);
},
watch: {
modelValue(newVal) {
console.log('值变化:', newVal);
}
}
}
javascript复制export default {
watch: {
'$route'(to, from) {
if (to.params.id !== from.params.id) {
this.fetchData(to.params.id);
}
}
},
methods: {
fetchData(id) {
// 获取新数据
}
}
}
javascript复制export default {
beforeRouteEnter(to, from, next) {
// 不能访问this
next(vm => {
console.log('组件实例:', vm);
});
},
beforeRouteUpdate(to, from, next) {
// 可以访问this
this.fetchData(to.params.id);
next();
},
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
if (confirm('有未保存的更改,确定离开吗?')) {
next();
} else {
next(false);
}
} else {
next();
}
}
}
javascript复制export default {
async serverPrefetch() {
// 服务端数据预取
this.posts = await fetch('/api/posts').then(res => res.json());
},
mounted() {
// 客户端数据获取
if (!this.posts.length) {
this.fetchPosts();
}
}
}
javascript复制export default {
created() {
performance.mark('component-created');
},
mounted() {
performance.mark('component-mounted');
performance.measure('创建到挂载', 'component-created', 'component-mounted');
}
}
javascript复制export default {
data() {
return {
observer: null
}
},
mounted() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入视口');
}
});
});
this.observer.observe(this.$el);
},
beforeUnmount() {
this.observer.disconnect();
}
}
javascript复制export default {
data() {
return {
socket: null,
messages: []
}
},
created() {
this.socket = new WebSocket('wss://example.com');
this.socket.onmessage = (event) => {
this.messages.push(JSON.parse(event.data));
};
},
beforeUnmount() {
if (this.socket) {
this.socket.close();
}
}
}
javascript复制export default {
data() {
return {
db: null
}
},
async mounted() {
this.db = await new Promise((resolve, reject) => {
const request = indexedDB.open('my-db', 1);
request.onerror = reject;
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('data', { keyPath: 'id' });
};
});
},
beforeUnmount() {
if (this.db) {
this.db.close();
}
}
}
javascript复制export default {
data() {
return {
peerConnection: null
}
},
mounted() {
this.peerConnection = new RTCPeerConnection();
// 设置事件监听等
},
beforeUnmount() {
if (this.peerConnection) {
this.peerConnection.close();
}
}
}
javascript复制export default {
data() {
return {
animationFrame: null
}
},
mounted() {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext('2d');
const animate = () => {
// 绘制逻辑
this.animationFrame = requestAnimationFrame(animate);
};
animate();
},
beforeUnmount() {
cancelAnimationFrame(this.animationFrame);
}
}
javascript复制export default {
data() {
return {
renderer: null,
scene: null,
camera: null
}
},
mounted() {
this.initThreeJS();
},
methods: {
initThreeJS() {
this.renderer = new THREE.WebGLRenderer({ canvas: this.$refs.canvas });
this.scene = new THREE.Scene();
this.camera = new THREE.PerspectiveCamera(75, 1, 0.1, 1000);
//