Vue生命周期钩子 父组件与子组件钩子的执行顺序分析
Vue 生命周期钩子概述
在深入探讨父组件与子组件钩子执行顺序之前,我们先来回顾一下 Vue 生命周期钩子的基础概念。Vue 实例从创建到销毁的过程,被称为生命周期。在这个过程中,Vue 提供了一系列的钩子函数,让开发者可以在特定的阶段执行自定义的逻辑。
常用的生命周期钩子
- beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时,实例的 data 和 methods 等属性还未初始化,不能访问到它们。
- created:实例已经创建完成,数据观测、属性和方法的运算、watch/event 事件回调都已配置好。但此时还没有开始挂载 DOM,$el 属性还不存在。
- beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用,即将把模板渲染成虚拟 DOM 并挂载到真实 DOM 上。
- mounted:实例被挂载后调用,此时 el 被新创建的 vm.$el 替换,并挂载到了实例上去。如果挂载的是一个文档内的元素,这时可以通过 this.$el 访问到真实 DOM 元素。
- beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在这个钩子中获取更新前的状态。
- updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。注意在这个钩子函数中操作数据可能会导致无限循环。
- beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用,可以执行一些清理任务,如清除定时器、解绑事件等。
- destroyed:实例销毁后调用。所有的事件监听器被移除,所有的子实例也都被销毁。
父组件与子组件钩子执行顺序 - 创建挂载阶段
为了更好地理解父组件与子组件钩子在创建挂载阶段的执行顺序,我们来看以下代码示例:
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>父子组件钩子顺序 - 创建挂载</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component></child - component>
</div>
<script>
Vue.component('child - component', {
template: '<div>子组件</div>',
beforeCreate() {
console.log('子组件 - beforeCreate');
},
created() {
console.log('子组件 - created');
},
beforeMount() {
console.log('子组件 - beforeMount');
},
mounted() {
console.log('子组件 - mounted');
}
});
new Vue({
el: '#app',
beforeCreate() {
console.log('父组件 - beforeCreate');
},
created() {
console.log('父组件 - created');
},
beforeMount() {
console.log('父组件 - beforeMount');
},
mounted() {
console.log('父组件 - mounted');
}
});
</script>
</body>
</html>
在上述代码中,我们定义了一个父组件 Vue
实例,以及一个子组件 child - component
。运行这段代码,在控制台中我们会看到输出顺序为:
- 父组件 - beforeCreate
- 父组件 - created
- 父组件 - beforeMount
- 子组件 - beforeCreate
- 子组件 - created
- 子组件 - beforeMount
- 子组件 - mounted
- 父组件 - mounted
这是因为在 Vue 实例创建过程中,首先是父组件开始初始化,执行 beforeCreate
和 created
钩子。接着进入挂载阶段,父组件调用 beforeMount
。然后开始创建子组件,子组件依次执行 beforeCreate
、created
和 beforeMount
。当子组件挂载完成(mounted
)后,父组件才最终完成挂载(mounted
)。
父组件与子组件钩子执行顺序 - 更新阶段
接下来,我们探讨在数据更新时,父组件与子组件钩子的执行顺序。我们对上述代码进行修改,增加数据更新的逻辑。
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>父子组件钩子顺序 - 更新</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component :msg="parentMsg"></child - component>
<button @click="updateParentMsg">更新父组件数据</button>
</div>
<script>
Vue.component('child - component', {
props: ['msg'],
template: '<div>{{msg}}</div>',
beforeUpdate() {
console.log('子组件 - beforeUpdate');
},
updated() {
console.log('子组件 - updated');
}
});
new Vue({
el: '#app',
data() {
return {
parentMsg: '初始值'
};
},
methods: {
updateParentMsg() {
this.parentMsg = '更新后的值';
}
},
beforeUpdate() {
console.log('父组件 - beforeUpdate');
},
updated() {
console.log('父组件 - updated');
}
});
</script>
</body>
</html>
在这个示例中,父组件通过 props
向子组件传递数据 parentMsg
。当点击按钮更新父组件的 parentMsg
数据时,我们观察控制台输出。执行顺序如下:
- 父组件 - beforeUpdate
- 子组件 - beforeUpdate
- 子组件 - updated
- 父组件 - updated
这是因为当父组件数据发生变化时,首先触发父组件的 beforeUpdate
钩子,表明父组件即将更新。由于子组件依赖于父组件传递的 props
,所以子组件也会进入更新流程,触发 beforeUpdate
。在子组件更新完成(updated
)后,父组件才最终完成更新(updated
)。
父组件与子组件钩子执行顺序 - 销毁阶段
最后,我们来看父组件与子组件在销毁阶段钩子的执行顺序。修改代码如下:
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>父子组件钩子顺序 - 销毁</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component v - if="isShow"></child - component>
<button @click="destroyChild">销毁子组件</button>
</div>
<script>
Vue.component('child - component', {
template: '<div>子组件</div>',
beforeDestroy() {
console.log('子组件 - beforeDestroy');
},
destroyed() {
console.log('子组件 - destroyed');
}
});
new Vue({
el: '#app',
data() {
return {
isShow: true
};
},
methods: {
destroyChild() {
this.isShow = false;
}
},
beforeDestroy() {
console.log('父组件 - beforeDestroy');
},
destroyed() {
console.log('父组件 - destroyed');
}
});
</script>
</body>
</html>
在这个例子中,通过点击按钮改变 isShow
的值来销毁子组件。当子组件被销毁时,控制台输出顺序为:
- 子组件 - beforeDestroy
- 子组件 - destroyed
- 父组件 - beforeDestroy
- 父组件 - destroyed
这表明在销毁过程中,首先是子组件进入销毁流程,执行 beforeDestroy
和 destroyed
钩子。只有当子组件完全销毁后,父组件才开始进入销毁流程,执行自己的 beforeDestroy
和 destroyed
钩子。
嵌套子组件的钩子执行顺序
在实际项目中,往往会存在多层嵌套的子组件。下面我们通过一个示例来看看多层嵌套子组件的钩子执行顺序。
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>多层嵌套子组件钩子顺序</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<parent - component></parent - component>
</div>
<script>
Vue.component('grand - child - component', {
template: '<div>孙组件</div>',
beforeCreate() {
console.log('孙组件 - beforeCreate');
},
created() {
console.log('孙组件 - created');
},
beforeMount() {
console.log('孙组件 - beforeMount');
},
mounted() {
console.log('孙组件 - mounted');
},
beforeUpdate() {
console.log('孙组件 - beforeUpdate');
},
updated() {
console.log('孙组件 - updated');
},
beforeDestroy() {
console.log('孙组件 - beforeDestroy');
},
destroyed() {
console.log('孙组件 - destroyed');
}
});
Vue.component('child - component', {
template: `
<div>
<grand - child - component></grand - child - component>
</div>
`,
beforeCreate() {
console.log('子组件 - beforeCreate');
},
created() {
console.log('子组件 - created');
},
beforeMount() {
console.log('子组件 - beforeMount');
},
mounted() {
console.log('子组件 - mounted');
},
beforeUpdate() {
console.log('子组件 - beforeUpdate');
},
updated() {
console.log('子组件 - updated');
},
beforeDestroy() {
console.log('子组件 - beforeDestroy');
},
destroyed() {
console.log('子组件 - destroyed');
}
});
Vue.component('parent - component', {
template: `
<div>
<child - component></child - component>
</div>
`,
beforeCreate() {
console.log('父组件 - beforeCreate');
},
created() {
console.log('父组件 - created');
},
beforeMount() {
console.log('父组件 - beforeMount');
},
mounted() {
console.log('父组件 - mounted');
},
beforeUpdate() {
console.log('父组件 - beforeUpdate');
},
updated() {
console.log('父组件 - updated');
},
beforeDestroy() {
console.log('父组件 - beforeDestroy');
},
destroyed() {
console.log('父组件 - destroyed');
}
});
new Vue({
el: '#app'
});
</script>
</body>
</html>
在创建挂载阶段,执行顺序为:
- 父组件 - beforeCreate
- 父组件 - created
- 父组件 - beforeMount
- 子组件 - beforeCreate
- 子组件 - created
- 子组件 - beforeMount
- 孙组件 - beforeCreate
- 孙组件 - created
- 孙组件 - beforeMount
- 孙组件 - mounted
- 子组件 - mounted
- 父组件 - mounted
在更新阶段,如果父组件数据变化影响到子组件和孙组件,执行顺序为:
- 父组件 - beforeUpdate
- 子组件 - beforeUpdate
- 孙组件 - beforeUpdate
- 孙组件 - updated
- 子组件 - updated
- 父组件 - updated
在销毁阶段,如果销毁父组件,执行顺序为:
- 孙组件 - beforeDestroy
- 孙组件 - destroyed
- 子组件 - beforeDestroy
- 子组件 - destroyed
- 父组件 - beforeDestroy
- 父组件 - destroyed
特殊情况 - 异步组件的钩子执行顺序
Vue 支持异步组件,异步组件在加载和渲染时有其独特的钩子执行顺序。我们来看一个异步组件的示例:
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>异步组件钩子顺序</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<async - component></async - component>
</div>
<script>
const AsyncComponent = () => ({
component: import('./AsyncComponent.vue'),
loading: {
template: '<div>加载中...</div>',
beforeCreate() {
console.log('异步组件加载占位 - beforeCreate');
},
created() {
console.log('异步组件加载占位 - created');
},
beforeMount() {
console.log('异步组件加载占位 - beforeMount');
},
mounted() {
console.log('异步组件加载占位 - mounted');
}
},
error: {
template: '<div>加载错误</div>',
beforeCreate() {
console.log('异步组件错误占位 - beforeCreate');
},
created() {
console.log('异步组件错误占位 - created');
},
beforeMount() {
console.log('异步组件错误占位 - beforeMount');
},
mounted() {
console.log('异步组件错误占位 - mounted');
}
},
delay: 200,
timeout: 3000
});
Vue.component('async - component', AsyncComponent);
new Vue({
el: '#app'
});
</script>
</body>
</html>
假设 AsyncComponent.vue
有如下代码:
<template>
<div>异步加载的组件</div>
</template>
<script>
export default {
beforeCreate() {
console.log('异步组件 - beforeCreate');
},
created() {
console.log('异步组件 - created');
},
beforeMount() {
console.log('异步组件 - beforeMount');
},
mounted() {
console.log('异步组件 - mounted');
}
};
</script>
在这个例子中,当页面加载时,首先会显示加载占位组件,其钩子依次执行。当异步组件加载完成后,加载占位组件被替换,异步组件的钩子开始执行。具体顺序为:
- 异步组件加载占位 - beforeCreate
- 异步组件加载占位 - created
- 异步组件加载占位 - beforeMount
- 异步组件加载占位 - mounted
- 异步组件 - beforeCreate
- 异步组件 - created
- 异步组件 - beforeMount
- 异步组件 - mounted
如果异步组件加载出错,错误占位组件的钩子会按顺序执行:
- 异步组件加载占位 - beforeCreate
- 异步组件加载占位 - created
- 异步组件加载占位 - beforeMount
- 异步组件加载占位 - mounted
- 异步组件错误占位 - beforeCreate
- 异步组件错误占位 - created
- 异步组件错误占位 - beforeMount
- 异步组件错误占位 - mounted
在实际项目中的应用场景
理解父组件与子组件钩子的执行顺序,在实际项目中有很多重要的应用场景。
数据初始化与共享
在创建挂载阶段,我们可以利用钩子顺序来进行数据的初始化和共享。例如,父组件在 created
钩子中获取数据,然后通过 props
传递给子组件。子组件在 created
或 mounted
钩子中根据接收到的 props
进行进一步的初始化操作。
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>数据初始化与共享</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component :user - data="userData"></child - component>
</div>
<script>
Vue.component('child - component', {
props: ['userData'],
template: '<div>{{userData.name}}</div>',
created() {
console.log('子组件根据接收到的 userData 进行操作');
}
});
new Vue({
el: '#app',
data() {
return {
userData: {
name: '张三'
}
};
},
created() {
console.log('父组件获取数据');
}
});
</script>
</body>
</html>
在这个例子中,父组件在 created
钩子获取数据,子组件在 created
钩子利用接收到的数据。
组件通信与状态管理
在更新阶段,了解钩子执行顺序有助于组件间的通信和状态管理。比如,父组件数据变化时,子组件根据 beforeUpdate
和 updated
钩子来同步更新自己的状态。
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>组件通信与状态管理</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component :count="parentCount"></child - component>
<button @click="incrementParentCount">增加父组件计数</button>
</div>
<script>
Vue.component('child - component', {
props: ['count'],
data() {
return {
localCount: this.count
};
},
beforeUpdate() {
this.localCount = this.count;
console.log('子组件同步父组件数据');
},
template: '<div>子组件计数: {{localCount}}</div>'
});
new Vue({
el: '#app',
data() {
return {
parentCount: 0
};
},
methods: {
incrementParentCount() {
this.parentCount++;
}
}
});
</script>
</body>
</html>
这里子组件在 beforeUpdate
钩子中同步父组件传递过来的 count
值,实现了组件间的状态同步。
资源清理与性能优化
在销毁阶段,正确利用钩子顺序可以进行资源清理和性能优化。比如,子组件在 beforeDestroy
钩子中清除定时器、解绑事件等,确保不会出现内存泄漏。
<!DOCTYPE html>
<html lang="zh - CN">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<title>资源清理与性能优化</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<child - component v - if="isShow"></child - component>
<button @click="destroyChild">销毁子组件</button>
</div>
<script>
Vue.component('child - component', {
template: '<div>子组件</div>',
data() {
return {
timer: null
};
},
created() {
this.timer = setInterval(() => {
console.log('子组件定时器运行');
}, 1000);
},
beforeDestroy() {
clearInterval(this.timer);
console.log('子组件清除定时器');
}
});
new Vue({
el: '#app',
data() {
return {
isShow: true
};
},
methods: {
destroyChild() {
this.isShow = false;
}
}
});
</script>
</body>
</html>
这样,在子组件销毁时,定时器被正确清除,避免了潜在的性能问题。
总结与注意事项
通过以上对 Vue 父组件与子组件生命周期钩子执行顺序的详细分析,我们可以看到不同阶段钩子的执行有其特定的规律。在实际开发中,正确掌握这些顺序对于编写健壮、高效的 Vue 应用至关重要。
需要注意的是,在钩子函数中执行的逻辑应该尽量简洁,避免复杂的计算和长时间的阻塞操作。特别是在 updated
钩子中,要防止因数据变化导致的无限循环更新。同时,在多层嵌套组件和异步组件的场景下,更要仔细梳理钩子执行顺序,以确保组件的正常运行和性能优化。
希望通过本文的讲解,读者能够对 Vue 父组件与子组件钩子的执行顺序有更深入的理解,并能在实际项目中灵活运用。