MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue生命周期钩子 父组件与子组件钩子的执行顺序分析

2024-08-165.4k 阅读

Vue 生命周期钩子概述

在深入探讨父组件与子组件钩子执行顺序之前,我们先来回顾一下 Vue 生命周期钩子的基础概念。Vue 实例从创建到销毁的过程,被称为生命周期。在这个过程中,Vue 提供了一系列的钩子函数,让开发者可以在特定的阶段执行自定义的逻辑。

常用的生命周期钩子

  1. beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时,实例的 data 和 methods 等属性还未初始化,不能访问到它们。
  2. created:实例已经创建完成,数据观测、属性和方法的运算、watch/event 事件回调都已配置好。但此时还没有开始挂载 DOM,$el 属性还不存在。
  3. beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用,即将把模板渲染成虚拟 DOM 并挂载到真实 DOM 上。
  4. mounted:实例被挂载后调用,此时 el 被新创建的 vm.$el 替换,并挂载到了实例上去。如果挂载的是一个文档内的元素,这时可以通过 this.$el 访问到真实 DOM 元素。
  5. beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在这个钩子中获取更新前的状态。
  6. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。注意在这个钩子函数中操作数据可能会导致无限循环。
  7. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用,可以执行一些清理任务,如清除定时器、解绑事件等。
  8. 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。运行这段代码,在控制台中我们会看到输出顺序为:

  1. 父组件 - beforeCreate
  2. 父组件 - created
  3. 父组件 - beforeMount
  4. 子组件 - beforeCreate
  5. 子组件 - created
  6. 子组件 - beforeMount
  7. 子组件 - mounted
  8. 父组件 - mounted

这是因为在 Vue 实例创建过程中,首先是父组件开始初始化,执行 beforeCreatecreated 钩子。接着进入挂载阶段,父组件调用 beforeMount。然后开始创建子组件,子组件依次执行 beforeCreatecreatedbeforeMount。当子组件挂载完成(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 数据时,我们观察控制台输出。执行顺序如下:

  1. 父组件 - beforeUpdate
  2. 子组件 - beforeUpdate
  3. 子组件 - updated
  4. 父组件 - 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 的值来销毁子组件。当子组件被销毁时,控制台输出顺序为:

  1. 子组件 - beforeDestroy
  2. 子组件 - destroyed
  3. 父组件 - beforeDestroy
  4. 父组件 - destroyed

这表明在销毁过程中,首先是子组件进入销毁流程,执行 beforeDestroydestroyed 钩子。只有当子组件完全销毁后,父组件才开始进入销毁流程,执行自己的 beforeDestroydestroyed 钩子。

嵌套子组件的钩子执行顺序

在实际项目中,往往会存在多层嵌套的子组件。下面我们通过一个示例来看看多层嵌套子组件的钩子执行顺序。

<!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>

在创建挂载阶段,执行顺序为:

  1. 父组件 - beforeCreate
  2. 父组件 - created
  3. 父组件 - beforeMount
  4. 子组件 - beforeCreate
  5. 子组件 - created
  6. 子组件 - beforeMount
  7. 孙组件 - beforeCreate
  8. 孙组件 - created
  9. 孙组件 - beforeMount
  10. 孙组件 - mounted
  11. 子组件 - mounted
  12. 父组件 - mounted

在更新阶段,如果父组件数据变化影响到子组件和孙组件,执行顺序为:

  1. 父组件 - beforeUpdate
  2. 子组件 - beforeUpdate
  3. 孙组件 - beforeUpdate
  4. 孙组件 - updated
  5. 子组件 - updated
  6. 父组件 - updated

在销毁阶段,如果销毁父组件,执行顺序为:

  1. 孙组件 - beforeDestroy
  2. 孙组件 - destroyed
  3. 子组件 - beforeDestroy
  4. 子组件 - destroyed
  5. 父组件 - beforeDestroy
  6. 父组件 - 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>

在这个例子中,当页面加载时,首先会显示加载占位组件,其钩子依次执行。当异步组件加载完成后,加载占位组件被替换,异步组件的钩子开始执行。具体顺序为:

  1. 异步组件加载占位 - beforeCreate
  2. 异步组件加载占位 - created
  3. 异步组件加载占位 - beforeMount
  4. 异步组件加载占位 - mounted
  5. 异步组件 - beforeCreate
  6. 异步组件 - created
  7. 异步组件 - beforeMount
  8. 异步组件 - mounted

如果异步组件加载出错,错误占位组件的钩子会按顺序执行:

  1. 异步组件加载占位 - beforeCreate
  2. 异步组件加载占位 - created
  3. 异步组件加载占位 - beforeMount
  4. 异步组件加载占位 - mounted
  5. 异步组件错误占位 - beforeCreate
  6. 异步组件错误占位 - created
  7. 异步组件错误占位 - beforeMount
  8. 异步组件错误占位 - mounted

在实际项目中的应用场景

理解父组件与子组件钩子的执行顺序,在实际项目中有很多重要的应用场景。

数据初始化与共享

在创建挂载阶段,我们可以利用钩子顺序来进行数据的初始化和共享。例如,父组件在 created 钩子中获取数据,然后通过 props 传递给子组件。子组件在 createdmounted 钩子中根据接收到的 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 钩子利用接收到的数据。

组件通信与状态管理

在更新阶段,了解钩子执行顺序有助于组件间的通信和状态管理。比如,父组件数据变化时,子组件根据 beforeUpdateupdated 钩子来同步更新自己的状态。

<!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 父组件与子组件钩子的执行顺序有更深入的理解,并能在实际项目中灵活运用。