一、概述

组件是带有名称的可复用实例, 是 Vue 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue 的编译器为它添加特殊功能。

通常一个应用会以一棵嵌套的组件树的形式来组织:

components.png

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册

二、 组件注册

1. 组件名

在字符串模板或单文件组件中定义组件时,定义组件名的方式有两种:

@使用 kebab-case

  1. app.component('my-component-name', {
  2. /* ... */
  3. })

当使用 kebab-case (短横线分隔命名) 定义一个组件时,你在引用这个自定义元素时也必须使用 kebab-case

@使用 PascalCase

  1. app.component('MyComponentName', {
  2. /* ... */
  3. })

当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。尽管如此,还是建议使用 kebab-case。

2. 全局注册

  1. // Create a Vue application
  2. const app = Vue.createApp({})
  3. // Define a new global component called button-counter
  4. app.component('button-counter', {
  5. data() {
  6. return {
  7. count: 0
  8. }
  9. },
  10. template: `
  11. <button @click="count++">
  12. You clicked me {{ count }} times.
  13. </button>`
  14. })

注意:data 必须是函数,且必须返回一个对象。

使用组件:

  1. <div id="app">
  2. <button-counter />
  3. <button-counter />
  4. <button-counter />
  5. </div>

3. 局部注册

  1. const ComponentA = {
  2. /* ... */
  3. }
  4. const ComponentB = {
  5. components: {
  6. 'component-a': ComponentA
  7. }
  8. // ...
  9. }

4. 模版字符串异常

在 Vue3 中,如果你使用 template 模板字符串定义组件,会抛出如下异常:

  1. [Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".

异常解读:组件提供了模板选项,但在此Vue构建中不支持运行时编译。 将你的 bundler 的别名 vue 配置为 vue/dist/vue.esm-bundle .js

解决方案:由于本教程主要基于 Vite 构建,所以这里主要讲解在 vite 中的处理方案。根据异常解读,不难发现,我们只需要配置 vue 别名即可。在 vite.config.js 文件中添加如下代码:

  1. import { defineConfig } from 'vite';
  2. import vue from '@vitejs/plugin-vue';
  3. // https://vitejs.dev/config/
  4. export default defineConfig({
  5. plugins: [vue()],
  6. // +++
  7. resolve: {
  8. alias: {
  9. vue: 'vue/dist/vue.esm-bundler.js',
  10. },
  11. },
  12. // +++
  13. });

5. 单文件组件(推荐)

5.1. 介绍

Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的 模板逻辑样式 封装在单个文件中。下面是 SFC 示例:

  1. <!-- 脚本 -->
  2. <script setup lang="ts">
  3. import { ref } from 'vue';
  4. const greeting = ref('Hello World!');
  5. </script>
  6. <!-- 模板 -->
  7. <template>
  8. <p class="greeting">{{ greeting }}</p>
  9. </template>
  10. <!-- 样式 -->
  11. <style scoped>
  12. .greeting {
  13. color: red;
  14. font-weight: bold;
  15. }
  16. </style>

提示:上述示例中,使用 Composition API 风格演示 ,因为 使用 Composition API 时更符合人体工程学的语法 >>

单文件组件由以下三种类型的顶层代码块组成:

  • <script setup>:JavaScript 模块(脚本,处理业务逻辑)
    • 每个 *.vue 文件最多可同时包含一个 <script setup>
    • 该脚本会被预处理并作为组件的 setup() 函数使用,也就是说它会在每个组件实例中执行。<script setup> 的顶层绑定会自动暴露给模板。
  • <template>: 组件模板(视图)
    • 每个 *.vue 文件最多可同时包含一个顶层 <template> 块。
    • 其中的内容会被提取出来并传递给 @vue/compiler-dom,预编译为 JavaScript 的渲染函数,并附属到导出的组件上作为其 render 选项。
  • <style>:样式
    • 一个 *.vue 文件可以包含多个 <style> 标签。
    • <style> 标签可以通过 scopedmodule 属性将样式封装在当前组件内。

查阅 SFC语法规范 >> 查看更多细节。

提示:推荐使用单文件组件~

5.2. 使用流程

单文件组件使用流程:创建单文件组件导入组件注册组件使用组件

提示:如果你使用 <script setup>,则无需注册,导入之后直接使用即可。

5.3. 关注点分离?

一些来自传统 Web 开发背景的用户可能会担心 SFC 在同一个地方混合了不同的关注点——HTML/CSS/JS 应该分开!

要回答这个问题,我们必须同意关注点分离不等于文件类型分离。工程原理的最终目标是提高代码库的可维护性。关注点分离,当墨守成规地应用为文件类型的分离时,并不能帮助我们在日益复杂的前端应用程序的上下文中实现该目标。

在现代 UI 开发中,我们发现与其将代码库划分为三个相互交织的巨大层,不如将它们划分为松散耦合的组件并进行组合更有意义。在组件内部,它的模板、逻辑和样式是内在耦合的,将它们搭配起来实际上可以使组件更具凝聚力可维护性

三、组件交互

1. defineProps & defineEmits

组件交互尽可能将父子组件解耦是很重要的,这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。

在 Vue 中,父子组件的交互可以总结为:

  • 父组件通过属性([props →](https://v3.cn.vuejs.org/guide/component-props.html)) 向子组件传递数据;
  • 子组件通过事件([emits →](https://v3.cn.vuejs.org/guide/component-custom-events.html)) 向父组件传递数据;

props-events.png

<script setup> 中必须使用 definePropsdefineEmits API 来声明 propsemits ,它们具备完整的类型推断并且在 <script setup> 中是直接可用的。

  • definePropsdefineEmits 都是只在 <script setup> 中才能使用的编译器宏。他们不需要导入且会随着 <script setup> 处理过程一同被编译掉。
  • defineProps 接收与 [props](https://v3.cn.vuejs.org/api/options-data.html#props) 选项 相同的值,defineEmits 也接收 [emits](https://v3.cn.vuejs.org/api/options-data.html#emits) 选项 相同的值。

@**defineProps**

接下来我们来看一组示例:

子组件

  1. <!-- child.vue -->
  2. <script setup lang="ts">
  3. // -- 声明属性类型(TS)
  4. interface IProps {
  5. name: string;
  6. age: number;
  7. job?: string;
  8. }
  9. // -- 定义属性
  10. const props = defineProps<IProps>();
  11. console.log(props);
  12. </script>
  13. <template>
  14. <div>{{ name }} - {{ age }} - {{ job }}</div>
  15. </template>

父组件

  1. <!-- parent.vue -->
  2. <script setup lang="ts">
  3. // 导入子组件(无需注册)
  4. import Child from './Child.vue';
  5. </script>
  6. <template>
  7. <!-- 使用子组件 -->
  8. <Child name="Li-HONGYAO" :age="28" job="前端工程师" />
  9. </template>

页面输出:

  1. Li-HONGYAO - 28 - 前端工程师

@属性默认值:**withDefaults**

使用 defineProps 定义属性时无法设置默认值,为了解决这个问题,提供了 withDefaults 编译器宏:

  1. const props = withDefaults(defineProps<IProps>(), {
  2. name: 'Muzili',
  3. age: 18,
  4. job: '未知',
  5. });

@**defineEmits**

1)首先,我们在 子组件 中定义事件:

  1. const emit = defineEmits<{
  2. (e: 'change', id: number):void;
  3. (e: 'update', value: string):void;
  4. }>();

上述示例中,e 对应事件名称,该名称可自行定义,idvalue 表示触发时间传递的参数。

2)在 子组件 模板中触发事件:

  1. <button type="button" @click="emit('change', 1)">触发[change]事件</button>
  2. <button type="button" @click="emit('update', 'Hello')">触发[update]事件</button>

3)在 父组件 中接收事件

  1. <script setup lang="ts">
  2. // -- 引入子组件
  3. import Child from './Child.vue';
  4. // -- 监听子组件[change]事件
  5. const onChange = (id: number) => {
  6. console.log(id);
  7. };
  8. // -- 监听子组件[update]事件
  9. const onUpdate = (value: string) => {
  10. console.log(value);
  11. };
  12. </script>
  13. <template>
  14. <Child name="Li-HONGYAO" :age="28" @change="onChange" @update="onUpdate" />
  15. </template>

2. defineExpose

暴露属性或方法给父组件使用,通过 defineExpose 实现:

子组件

  1. <script setup lang="ts">
  2. // -- 变量
  3. const name = 'Li-HONGYAO';
  4. // -- 方法
  5. const sayHello = (name: string) => {
  6. console.log(`Hello, ${name}!`);
  7. };
  8. // -- 定义子子组件暴露出去的属性的类型声明
  9. export interface ExposeProps {
  10. name: string;
  11. sayHello: (name: string) => void;
  12. }
  13. // -- 将变量 name 和方法 sayHello 暴露给父组件
  14. // -- 父组件可通过 ref 访问
  15. defineExpose({
  16. name,
  17. sayHello,
  18. });
  19. </script>
  20. <template></template>

父组件

  1. <script setup lang="ts">
  2. import { onMounted, ref } from 'vue';
  3. import Child, { ExposeProps } from './Child.vue';
  4. const childRef = ref<ExposeProps>();
  5. onMounted(() => {
  6. // 访问子组件属性:name
  7. console.log(childRef.value?.name); // Li-HONGYAO
  8. // 调用子组件方法:sayHello
  9. childRef.value?.sayHello('Li-HONGYAO'); // Hello, Li-HONGYAO!
  10. });
  11. </script>
  12. <template>
  13. <Child ref="childRef" />
  14. </template>

3. v-mode

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。

当需要使用多个 v-model 或者说你想要实现子组件某个特定属性的 v-model 时,比如 count 属性,我们可以这样做,这里以简单封装一个 Counter 组件为例:

子组件:**src/components/Counter.vue**

  1. <script setup lang="ts">
  2. interface IProps {
  3. min?: number;
  4. max?: number;
  5. count?: number;
  6. }
  7. const props = withDefaults(defineProps<IProps>(), {
  8. min: 1,
  9. max: 5,
  10. count: 1,
  11. });
  12. const emit = defineEmits<{
  13. (e: 'update:count', count: number): void;
  14. }>();
  15. // events
  16. const plus = () => {
  17. const { count, max } = props;
  18. emit('update:count', count + 1 > max ? max : count + 1);
  19. };
  20. const minus = () => {
  21. const { count, min } = props;
  22. emit('update:count', count - 1 < min ? min : count - 1);
  23. };
  24. </script>
  25. <template>
  26. <div class="wrap">
  27. <span>子组件:</span>
  28. <button type="button" @click="minus"></button>
  29. <div class="v">{{ count }}</div>
  30. <button type="button" @click="plus"></button>
  31. </div>
  32. </template>
  33. <style scoped>
  34. .wrap {
  35. display: flex;
  36. justify-content: flex-start;
  37. align-items: center;
  38. }
  39. button {
  40. width: 30px;
  41. height: 30px;
  42. display: flex;
  43. justify-content: space-between;
  44. align-items: center;
  45. cursor: pointer;
  46. }
  47. .v {
  48. text-align: center;
  49. width: 50px;
  50. font-weight: bold;
  51. }
  52. </style>

父组件:**src/App.vue**

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. import Counter from './components/Counter.vue';
  4. const count = ref(1);
  5. </script>
  6. <template>
  7. <Counter v-model:count="count" />
  8. <p>父组件:商品数量 → {{ count }}</p>
  9. </template>

演示效果

v-model_update.gif

4. 单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件 意外改变 父组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 props 这样是不规范的写法,开发环境会报警告。

如果实在要改变父组件的 props 值可以再 data 里面定义一个变量,并用 prop 的值初始化它,之后用 $emit 通知父组件去修改。

5. Provide & Inject

通常,当我们需要从父组件向子组件传递数据时,我们使用 props,想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件需要访问顶层组件的属性。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provideinject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这些数据。

components_provide.png

接下来,我们看一组示例,在顶层组件中通过 Provide 传递一组数据,在子组件中通过 Inject 接收:

顶层组件:

  1. <script setup lang="ts">
  2. import { provide } from 'vue';
  3. provide("global", {
  4. env: "development",
  5. appID: 'xxx'
  6. });
  7. </script>

其中,global 表示 key,底层组件可通过 inject(key) 访问, 后面跟的对象表示传递的值。

底层组件:

  1. <script setup lang="ts">
  2. import { inject } from 'vue';
  3. const global = inject('global');
  4. console.log(global); // → {env: 'development', appID: 'xxx'}
  5. </script>

6. $attrs

$attr 主要用于获取没有在组件内部通过 props 或者 emits 明确定义的属性。我们来看一组示例:

子组件

  1. <script setup lang="ts">
  2. defineProps<{ name: string }>();
  3. </script>

在子组件中,定义了 name 属性。

父组件

  1. <child-comp name="Li-HONGYAO" job="Senior Front-End Developer" />

父组件在调用子组件时,除了传递子组件需要的 name 属性之外,还传递了 job 属性,该属性并没有在 props 中定义,接下来我们通过 useAttrs 来访问它。

  1. <script setup lang="ts">
  2. // +++
  3. import { useAttrs } from 'vue';
  4. // +++
  5. defineProps<{ name: string }>();
  6. // +++
  7. // -- 获取非props属性
  8. const attrs = useAttrs();
  9. console.log(attrs); // → Proxy {job: 'Senior Front-End Developer', __vInternal: 1}
  10. // +++
  11. </script>

可以看到,访问 attrs 变量,输出了 job 信息。

@Attribute 继承

非props属性具有 隐式贯穿 行为, 如果你在根组件中(或者任意父组件中)使用非props属性,那么它将会传递给其所有的子组件。如果你想要的禁用 attribute 继承,可以在组件的选项中设置 inheritAttrs: false

  1. <!-- <script setup> & <script> -->
  2. <script>
  3. export default {
  4. inheritAttrs: false,
  5. };
  6. </script>
  7. <script setup lang="ts"></script>

注意:<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:

  • 无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。
  • 声明命名导出。
  • 运行副作用或者创建只需要执行一次的对象。

7. $parent & $children

在vue2.x 以及 vue3.x 的选项式API中,允许使用 this.$parentthis.$children 获取当前组件的父组件和当前组件的子组件。但是在 vue3.x setup 中,想要调用父组件的方法,我们需要用 provide & inject 来实现。

四、插槽

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

1. 插槽内容

  1. <script setup lang="ts">
  2. defineProps<{ url: string }>();
  3. </script>
  4. <template>
  5. <a :href="url">
  6. <!-- 接收插槽内容 -->
  7. <slot />
  8. </a>
  9. </template>
  1. <navigation-link url="/login">前往登录</navigation-link>
  2. <navigation-link url="/register">前往注册</navigation-link>

渲染效果:

  1. <a href="/login">前往登录</a>
  2. <a href="/register">前往注册</a>

提示:插槽样式在子父组件中都可以设置,所以在命名class时一定要注意。

2. 渲染作用域

当你想在一个插槽中使用数据时,例如:

  1. <navigation-link url="/profile">
  2. Logged in as {{ user.name }}
  3. </navigation-link>

该插槽可以访问与模板其余部分相同的实例属性 (即相同的“作用域”),所以这里不能访问 <navigation-link > 的作用域。例如 url 是访问不到的:

  1. <navigation-link url="/profile">
  2. <!-- → Property "url" was accessed during render but is not defined on instance. -->
  3. <span>Clicking here will send you to: {{ url }}</span>
  4. </navigation-link>

简单理解就是,现有组件 AB,在 A 组件中通过插槽的形式将内容分发给组件 B,尽管插槽内容最终是在 B 组件中渲染的,但是在插槽内容也只能访问组件 A 中的属性,不能访问组件 B 中的属性。

请记住这条规则:

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

3. 后备内容

为插槽指定默认值,它只会在没有提供内容的时候被渲染

  1. <a :href="url">
  2. <!-- 接收插槽内容 -->
  3. <slot>默认内容</slot>
  4. </a>
  1. <navigation-link url="/orders"></navigation-link>

渲染效果:

  1. <a href="/orders">默认内容</a>

4. 具名插槽

有时我们需要多个插槽,但是插槽内容会重复,如下所示:

  1. <template>
  2. <!-- 期望在这里展示姓名 -->
  3. <slot></slot>
  4. <hr color="red" />
  5. <!-- 期望在这里展示职位 -->
  6. <slot></slot>
  7. </template>
  1. <template>
  2. <Layout>
  3. <p>Li-HONGYAO</p>
  4. <small>Senior Front-End Developer</small>
  5. </Layout>
  6. </template>

运行效果:

slot_named_1.png

可以看到,上述示例展示的效果并没有根据期望来渲染,为了解决这个问题,我们可以给插槽命名,进行相应绑定,我们修改一下示例:

  1. <template>
  2. <div>
  3. <b>Name:</b>
  4. <!-- 具名插槽:name -->
  5. <slot name="name"></slot>
  6. </div>
  7. <div>
  8. <b>Gender:</b>
  9. <!-- 默认插槽:default -->
  10. <slot></slot>
  11. </div>
  12. <div>
  13. <b>job:</b>
  14. <!-- 具名插槽:job -->
  15. <slot name="job"></slot>
  16. </div>
  17. <hr color="red" />
  18. </template>
  1. <template>
  2. <Layout>
  3. <span>male</span>
  4. <template v-slot:name>Li-HONGYAO</template>
  5. <template v-slot:job>Senior Front-End Developer</template>
  6. </Layout>
  7. </template>

运行效果:

slot_named_2.png

提示:v-slot 指令可以使用 # 替代,比如:v-slot:job 可以缩写为 #job

注意:**v-slot** 只能添加在 **<template>**

5. 作用域插槽

作用域插槽是一种特殊类型的插槽,有时让插槽内容能够访问子组件中才有的数据是很有用的。

简单理解就是:父组件决定布局,数据由子组件提供。先来看示例:

  1. <script setup lang="ts">
  2. import { reactive } from 'vue';
  3. const state = reactive({
  4. name: 'Li-HONGYAO',
  5. job: 'Senior Front-End Developer',
  6. });
  7. </script>
  8. <template>
  9. <!-- 通过属性将state数据传递出去 -->
  10. <slot :scope="state" />
  11. </template>
  1. <template>
  2. <Layout>
  3. <template #default="{ scope: { name, job } }">
  4. <!-- name -->
  5. <div>
  6. <b>Name:</b>
  7. <span>{{ name }}</span>
  8. </div>
  9. <!-- job -->
  10. <div>
  11. <b>Job:</b>
  12. <span>{{ job }}</span>
  13. </div>
  14. <hr color="red" />
  15. </template>
  16. </Layout>
  17. </template>

运行效果:

slot_scope.png

注意:作用域插槽不能和具名插槽混合使用。

五、动态组件

通过使用保 <component> 元素,动态地绑定到它的 is 特性,可以实现组件的动态切换,这对于多标签页是非常有用的。

  1. <script setup lang="ts">
  2. import { reactive, shallowRef, defineComponent } from 'vue';
  3. // -- 定义组件
  4. const Home = defineComponent({
  5. template: `<div class="page">This is Home page.</div>`
  6. })
  7. const News = defineComponent({
  8. template: `<div class="page">This is News page.</div>`
  9. })
  10. const Mine = defineComponent({
  11. template: `<div class="page">This is Mine page.</div>`
  12. })
  13. // -- 定义状态
  14. const state = reactive({
  15. currentTab: shallowRef(Home),
  16. tabs: ['Home', 'News', 'Mine'],
  17. });
  18. // -- 时间处理
  19. const switchTab = (key: string) => {
  20. switch(key) {
  21. case 'Home': state.currentTab = Home; break;
  22. case 'News': state.currentTab = News; break;
  23. case 'Mine': state.currentTab = Mine; break;
  24. }
  25. }
  26. </script>
  27. <template>
  28. <div class="wrap">
  29. <button
  30. type="button"
  31. v-for="(item, index) in state.tabs"
  32. :key="index"
  33. @click="switchTab(item)"
  34. >
  35. {{ item }}
  36. </button>
  37. <component :is="state.currentTab"></component>
  38. </div>
  39. </template>
  40. <style scoped>
  41. button {
  42. margin-bottom: 16px;
  43. }
  44. </style>

展示效果:

dynamic-compnenet.gif

keep-alive

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复渲染导致的性能问题。为此可以用一个 <keep-alive> 元素将其动态组件包裹起来。

  1. <keep-alive>
  2. <component :is="state.currentTab"></component>
  3. </keep-alive>

六、生命周期钩子函数执行顺序

接下来我们讨论父子组件生命周期钩子函数的执行顺序,当然,这里不以 <script setup> 来讲,主要以 Options API 来讲。

为了便于大家可以更加直观的去观察生命周期的执行顺序,我们先通过 Options API 构建如下组件:

子组件:src/components/Child.vue

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. export default defineComponent({
  4. props: {
  5. msg: String,
  6. },
  7. beforeCreate() {
  8. console.log('子:__beforeCreate__');
  9. },
  10. created() {
  11. console.log('子:__created__');
  12. },
  13. beforeMount() {
  14. console.log('子:__beforeMount__');
  15. },
  16. mounted() {
  17. console.log('子:__mounted__');
  18. },
  19. beforeUpdate() {
  20. console.log('子:__beforeUpdate__');
  21. },
  22. updated() {
  23. console.log('子:__updated__');
  24. },
  25. beforeUnmount() {
  26. console.log('子:__beforeUnmount__');
  27. },
  28. unmounted() {
  29. console.log('子:__unmounted__');
  30. },
  31. });
  32. </script>
  33. <template>
  34. <div>This is Child Component.</div>
  35. <p>msg:{{ msg }}</p>
  36. </template>

父组件:src/components/Parent.vue

  1. <script lang="ts">
  2. import { defineComponent } from 'vue';
  3. import Child from './Child.vue';
  4. export default defineComponent({
  5. components: {
  6. Child,
  7. },
  8. data() {
  9. return {
  10. msg: 'Hello, vue!',
  11. };
  12. },
  13. beforeCreate() {
  14. console.log('父:__beforeCreate__');
  15. },
  16. created() {
  17. console.log('父:__created__');
  18. },
  19. beforeMount() {
  20. console.log('父:__beforeMount__');
  21. },
  22. mounted() {
  23. console.log('父:__mounted__');
  24. },
  25. beforeUpdate() {
  26. console.log('父:__beforeUpdate__');
  27. },
  28. updated() {
  29. console.log('父:__updated__');
  30. },
  31. beforeUnmount() {
  32. console.log('父:__beforeUnmount__');
  33. },
  34. unmounted() {
  35. console.log('父:__unmounted__');
  36. },
  37. });
  38. </script>
  39. <template>
  40. <Child :msg="msg" />
  41. <button type="button" @click="msg = 'Hello, world!'">更新属性:msg</button>
  42. </template>

根组件:src/App.vue

  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. const removeParent = ref(false);
  4. </script>
  5. <template>
  6. <Parent v-if="!removeParent" />
  7. <button type="button" @click="removeParent = true">销毁父组件</button>
  8. </template>

注意:由于根组件只用于销毁父组件(Parent),所以这个组件我使用的是 组合式API(<script setup>).

接下来,我们启动项目,观察父子组件在渲染、更新和销毁时,钩子函数的执行顺序,请看示例动图:

comp_running_sequence.gif

通过上述示例,可以得出如下结论:

  • 加载渲染过程父:beforeCreate父:created父:beforeMount子:beforeCreate子:created子:beforeMount子:mounted父:mounted

  • 子组件更新过程父:beforeUpdate子:beforeUpdate子:updated父:updated

  • 父组件更新过程父:beforeUpdate父:updated

  • 销毁过程父:beforeUnmount子:beforeUnmount子:unmounted父:unmounted