组件生命周期

Vue 中,每个组件都有自己的生命周期,所谓生命周期,指的是组件自身的一些方法(或者叫作钩子函数),这些方法在特殊的时间点或遇到一些特殊的框架事件时会被自动触发。Vue 组件的生命周期如图3-1所示。

Figure P74 50412
Figure 1. 图3-1 Vue 组件的生命周期
image 2024 11 27 23 15 18 651

可以看到,在 Vue 组件的整个生命周期中会有很多钩子函数可供使用,在生命周期不同的时刻可以执行不同的操作。下面列出所有的钩子函数:

beforeCreate

beforeUpdate

activated

errorCaptured

created

updated

deactivated

beforeMount

beforeUnmount

renderTriggered

mounted

unmounted

renderTracked

主要 Vue 生命周期事件被分为两个钩子,分别在事件之前和之后调用,vue 应用程序中有 4 个主要事件(8 个钩子):

  • 创建 ---- 在组建创建时执行

  • 挂载 ---- DOM 被挂载时执行

  • 更新 ---- 当响应数据被修改时执行

  • 销毁 ---- 在元素被销毁之前立即执行

在学习组件的生命周期之前,建议读者先编写一个简单的页面,以实际体验每个钩子函数的触发顺序和时机。运行之后,可以在控制台中看到具体的触发时机,当然某些方法在特定的场景才会触发。以下是具体的示例:

const vm = Vue.createApp({
    data(){
        return {
            message: 'Vue组件的生命周期'
        }
    },
    beforeCreate() {
        console.log('------beforeCreate------');
    },
    created() {
        console.log('------created------');
    },
    beforeMount() {
        console.log('------beforeMount------');
    },
    mounted() {
        console.log('------mounted------');
    },
    beforeUpdate () {
        console.log('------beforeUpdate------');
    },
    updated () {
        console.log('------updated------');
    },
    beforeUnmount () {
        console.log('------beforeUnmount------');
    },
    unmounted () {
        console.log('------unmounted------');
    },
    activated () {
        console.log('------activated------');
    },
    deactivated () {
        console.log('------deactivated------');
    },
    errorCaptured() {
        console.log('------errorCaptured------');
    },
}).mount("#app")

下面将详细介绍组件的生命周期内各个方法的含义和用法。

beforeCreate 和 created

beforeCreate 方法

这个阶段在实例初始化之后,数据观测(Data Observer)和 event/watcher 事件配置之前被调用。需要注意的是,这个阶段无法获取到 Vue 组件 data 中定义的数据,官方也不推荐在这里操作 data,如果确实需要获取 data,可以从 this.$options.data() 获取。

created 方法

beforeCreate 执行完成之后,Vue 会执行一些数据观测和 event/watcher 事件的初始化工作,将数据和 data 属性进行绑定以及对 propsmethodswatch 等进行初始化,另外还要初始化一些 injectprovide

可以从图3-2来了解 beforeCreate 方法和 created 方法的主要流程与执行逻辑。

Figure P76 50415
Figure 2. 图3-2 beforeCreate 方法和 created 方法的主要流程与执行逻辑

从图3-2可以得知,在 created 方法执行时,template 也是一个关键设置,如果当前 Vue 组件设置了 template 属性,则将其作为模板编译成 render 函数,即 template 中的 HTML 内容会渲染到 el 节点内部。如果当前 Vue 组件没有设置 template 属性,则将当前 el 节点所在的 HTML 元素(即 el.outerHTML)作为模板进行编译。因此,如果组件没有设置 template 属性,就相当于设置了内容是 el 节点的 template。需要注意的是,这个阶段并没有真正地把 template 或者 el 渲染到页面上,只是先将内容准备好(即把 render 函数准备好)。

在使用 created 钩子函数时,通常执行一些组件的初始化操作或者定义一些变量,如果是一个表格组件,那么在 created 时就可以调用 API 接口,开始发送请求来获取表格数据等。

在组合 API 中,使用 setup() 方法替换了 beforeCreatecreated,那么在这两个生命周期中的方法将放在 setup 中执行。

beforeMount 和 mounted

beforeMount 方法

前文提到了 eltemplate 属性以及 render 函数,render 函数用于给当前 Vue 实例挂载 DOMVue 组件渲染 HTML 内容),这里的 beforeMount 就是渲染前要执行的程序逻辑。

mounted 方法

这个阶段开始真正地执行 render 方法进行渲染,之前设置的 el 会被 render 函数执行的结果所替换,也就是说将结果真正渲染到当前 Vue 实例的 el 节点上,这时就会调用 mounted 方法。从图3-3可以看到 beforeMount 方法和 mounted 方法的主要流程与执行逻辑。

image 2024 02 21 15 18 57 578
Figure 3. 图3-3 beforeMount 方法和 mounted 方法的主要流程与执行逻辑

mounted 这个钩子函数的使用频率非常高,当触发这个函数时,就代表组件的用户界面已经渲染完成,可以在 DOM 中获取这个节点。通常用这个方法执行一些用户界面节点获取的操作,例如在 Vue 中使用一个 jQuery 插件,在这个方法中就可以获得插件所依赖的 DOM,从而进行初始化。

但是需要注意的是,mounted 不会保证所有的子组件也都一起被挂载。如果读者希望等到整个视图都渲染完毕,可以在 mounted 内部使用 this.$nextTick,代码如下:

mounted() {
    this.$nextTick(function () {
        // 仅在渲染整个视图之后运行的代码
    })
}

beforeUpdate 和 updated

前面讲解的生命周期函数在调用 Vue.createApp({})mount(el) 方法时就会触发,我们可以把它们归类成实例初始化时自动调用的钩子函数,而 beforeUpdateupdated 这两个方法若要触发,则需要特定的场景。

beforeUpdate 方法

Vue 实例 data 中的数据发生了改变,就会触发对应组件的重新渲染,这是双向绑定的特性之一,所以数据改变就会触发 beforeUpdate 方法。

updated 方法

当执行完 beforeUpdate 方法后,就会触发当前组件挂载 DOM 内容的修改,当前 DOM 修改完成后,便会触发 updated 方法,在 updated 方法中可以获取更新之后的 DOM

下面用代码来模拟 beforeUpdateupdated 的触发时机,如示例代码3-1-1所示。

<div id="app">
    {{message}}
    <button @click="clickCallback">点击</button>
</div>
const vm = Vue.createApp({
    data() {
        return {
            message: 'I am Tom'
        }
    },
    beforeCreate() {
        console.log('------beforeCreate------');
    },
    created() {
        console.log('------created------');
    },
    beforeMount() {
        console.log('------beforeMount------');
    },
    mounted() {
        console.log('------mounted------');
    },
    beforeUpdate () {
        console.log('------beforeUpdate------');
    },
    updated () {
        console.log('------updated------');
    },
    methods:{
        clickCallback: function(){
            this.message = 'I am Jack'
        }
    }
}).mount('#app')

运行这段代码后,会依次看到 beforeCreatecreatedbeforeMountmounted 方法打印在 Chrome 浏览器的控制台上;单击按钮,会看到文字由 “I am Tom” 变成了 “I am Jack”;然后在控制台上可以看到依次打印了 beforeUpdateupdated,如图3-4所示。

image 2024 02 21 15 27 22 334
Figure 4. 图3-4 beforeUpdate 和 updated 的触发时机

由此可知,这两个方法是可以触发或者执行多次的,所以在 Vue 组件的生命周期中,每当 data 中的值被修改都会执行这两个方法。执行流程如图3-5所示。

image 2024 02 21 15 28 15 414
Figure 5. 图3-5 beforeUpdate方法和updated方法的执行流程

前面讲解了双向绑定中 Model 影响 DOM 的具体体现,在 MVVM 模式的 Model,也就是 Vuedata 的值发生改变时,会触发 beforeUpdateupdated 这两个方法。下面就来演示双向绑定中 DOM 影响 Model 的具体体现,同样也会触发这两个方法,如示例代码3-1-2所示。

示例代码3-1-2 updated 方法和 beforeUpdate 方法
<div id="app">
    <input type="text" v-model="message">
</div>
const vm = Vue.createApp({
    data() {
        return {
            message: 'I am Tom'
        }
    },
    beforeUpdate () {
        console.log(this.message)
        console.log('------beforeUpdate------');
    },
    updated () {
        console.log(this.message)
        console.log('------updated------');
    }
}).mount('#app')

在上面的代码中,使用了 <input> 标签,并给 <input> 设置了 v-model 指令,表示与 data 中的 message 进行关联,这时修改 <input> 中的内容就是修改了 DOM 的内容,可以在控制台中看到会触发 beforeUpdateupdated 这两个方法,同时 message 的值也在实时变动,这就是双向绑定中 DOM 影响 Model 的具体体现。Chrome 浏览器的控制台如图3-6所示。

image 2024 02 21 15 32 02 076
Figure 6. 图3-6 双向绑定中 beforeUpdate 方法和 updated 方法的触发时机

v-model 指令不仅可以作用在 <input> 上,还可以作用在自定义组件上,我们会在后面讲解。

beforeUnmount 和 unmounted

组件卸载,正如万物有生有灭一样,既然组件有创建,也就必然有消亡。如果频繁调用创建的代码,但是一直没有清除,就会造成内存飙升,而且一直不释放,还有可能导致 “内存泄漏” 问题,这也是卸载组件的意义。

beforeUnmount 方法

beforeUnmount 方法在组件卸载之前调用。在这一步,实例仍然完全可用。

unmounted 方法

unmounted 方法在 Vue 组件卸载后调用。调用后,Vue 组件关联的所有事件监听器都会被移除,所有的当前组件的子组件也会被销毁。

在触发卸载操作之后,首先会将当前组件从其父组件中清除,然后清除当前组件的事件监听和数据绑定,清除一个 Vue 组件可以简单理解为将 Vue 对象关联的一些数据类型的变量清空或者置为 null

一般来说,卸载组件常发生在采用 v-if 指令进行逻辑判断时,如示例代码3-1-3所示。

示例代码3-1-3 beforeUnmount 方法和 unmounted 方法
<div id="app">
    <button @click="flag = 2">点我</button>
    <componenta v-if="flag == 1"></componenta>
    <componentb v-else></componentb>
</div>
const componenta = {
    template: '<h2>myComponent a</h2>',
    beforeUnmount(){
        console.log('------componenta:beforeUnmount------');
    },
    unmounted(){
        console.log('------componenta:unmounted------');
    }
}
const componentb = {
    template: '<h2>myComponent b</h2>',
    beforeMount(){
        console.log('------componentb:beforeMount------');
    },
    mounted(){
        console.log('------componentb:mounted------');
    }
}
const app = Vue.createApp({
    data(){
        return {
            flag: 1
        }
    },
    components: {
        componenta:componenta,
        componentb:componentb
    }
}).mount('#app')

当我们单击按钮时,flag 值被设置为 2,这就触发了 v-if 指令的逻辑,<componenta> 组件卸载,<componentb> 组件挂载,控制台打印的日志如图3-7所示。

image 2024 02 21 15 40 20 209
Figure 7. 图3-7 beforeUnmount 和 unmounted 方法

可以看到 <componenta> 组件卸载时,触发了 beforeUnmountunmounted<componentb> 组件挂载时触发了 beforeMountmounted。如果想要主动触发对根组件的卸载,可以调用根实例的 unmount() 方法,调用之后就会清理与其他实例的联系,解绑它的全部指令及事件监听器。

errorCaptured

该方法表示当捕获一个来自当前子孙组件的错误时被触发,注意当前组件报错不会触发。这里的报错一般只会限制在当前 Vue 根实例下代码所抛出的 DOMException 或者异常 Error 对象(new Error())等错误,如果是 Vue 之外的代码,是不会触发的。该方法会收到三个参数:错误对象发生错误的组件实例 以及 一个包含错误来源信息的字符串。在某个子孙组件的 errorCaptured 返回 false 时,可以阻止该错误继续向上传播。

另外,也可以在全局配置 errorCaptured,这样就可以监听到所有属于该根实例的报错信息,配置如示例代码3-1-4所示。

示例代码3-1-4 errorCaptured 方法
const app = Vue.createApp({
    mounted(){
        throw new Error('err')
    }
})
app.config.errorHandler = (err, vm, info) => {
    console.error(err)
    console.log(vm)
    // 'info' 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
    console.log(info)
}
app.mount('#app')

在浏览器控制台查看打印的报错信息,如图3-8所示。

image 2024 02 21 15 44 53 992
Figure 8. 图3-8 errorCaptured 捕捉错误信息

如果某个子孙组件 errorCaptured 方法返回 false 以阻止错误继续向上传播,那么它会阻止其他任何会被这个错误唤起的 errorCaptured 方法和全局的 config.errorHandler 的触发。

错误传递规则:

  • 默认情况下,所有的错误都会被发送到应用级的 app.config.errorHandler (前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。

  • 如果组件的继承链或组件链上存在多个 errorCaptured 钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为 “向上传递”,类似于原生 DOM 事件的冒泡机制。

  • 如果 errorCaptured 钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler

  • errorCaptured 钩子可以通过返回 false 来阻止错误继续向上传递。即表示 “这个错误已经被处理了,应当被忽略”,它将阻止其他的 errorCaptured 钩子或 app.config.errorHandler 因这个错误而被调用。

activated 和 deactivated

keep-alive 的作用:

<keep-alive> 是一个内置组件,用于缓存包裹的组件。这样,当切换组件时,组件不会被销毁,而是会被保留在内存中并等待重新激活。因此,activateddeactivated 钩子只在被 <keep-alive> 包裹的组件中才会触发。

  • activateddeactivated 只对被 <keep-alive> 缓存的组件有效,如果组件没有被 <keep-alive> 包裹,这两个钩子不会被触发。

  • 这两个钩子不会在每次视图切换时都被调用,而是只会在组件状态改变时触发。

这两个方法并不是标准的 Vue 组件的生命周期方法,它们的触发时机需要结合 vue-router 及组件 keep-alive 来使用。

当你在 Vue 应用中使用 vue-router 进行页面路由切换时,若将一些组件用 <keep-alive> 包裹,它们将被缓存并且不会在页面切换时被销毁。这样可以提高性能,避免不必要的重渲染。此时,activateddeactivated 钩子与 <keep-alive> 配合使用,触发时机是:

  • activated:当 <keep-alive> 包裹的组件恢复显示时,意味着组件在路由切换时被激活,从缓存中恢复并显示时触发。

  • deactivated:当 <keep-alive> 包裹的组件不再显示时,组件被缓存起来,停用时触发。

这两个钩子特别有助于在路由切换时管理组件的状态,比如重新发起请求、清除定时器等。

renderTracked 和 renderTriggered

renderTrackedrenderTriggeredVue 3 中与响应式系统相关的调试工具,它们是 Vue 3 响应式系统内部用于追踪渲染过程时变化的两个钩子函数,通常用于开发和调试阶段。它们并不是用户直接使用的 API,而是 Vue 的调试工具,帮助开发者在开发过程中更好地理解组件的更新机制。

renderTracked

renderTracked 钩子函数在 响应式属性被访问并用于组件渲染时 被调用。它通常在 渲染过程 中触发,指示某个响应式数据被 追踪(即依赖收集)。

你可以在组件渲染期间监听哪些响应式属性被访问,并在 renderTracked 中进行处理。以下是 renderTracked 的用法示例:

import { createApp, reactive, effect } from 'vue';
import { renderTracked } from 'vue';

const app = createApp({
  setup() {
    // 创建一个响应式数据对象
    const state = reactive({
      count: 0,
      name: 'Vue'
    });

    // 使用 renderTracked 来监听依赖追踪
    renderTracked((e) => {
      // e 是一个事件对象,包含了相关的追踪信息
      console.log('Tracked Dependency:', e);
    });

    // 创建一个 effect 来观察 state 数据
    effect(() => {
      console.log(state.count);  // 访问了 state.count,触发依赖追踪
    });

    // 修改响应式数据
    state.count++;  // 这将触发 renderTracked 和 effect

    return { state };
  }
});

app.mount('#app');

renderTracked 监听的事件对象:

当你访问某个响应式数据的 getter 时,renderTracked 会被触发,并且它的回调函数会接收到一个事件对象,该对象包含了关于依赖追踪的详细信息。具体来说,它包含以下字段:

  • effect:表示当前的 effect(响应式副作用)对象。

  • target:表示当前的响应式对象(例如 state)。

  • key:表示被访问的响应式数据的属性名(例如 count)。

  • type:表示属性的类型,可能是 getsethas 等。

如何在开发中使用 renderTracked

  • 性能优化:使用 renderTracked 来观察哪些响应式数据被访问,帮助你找出组件的重新渲染触发点。通过这个工具,你可以知道哪些数据实际上对当前的渲染产生了影响。

  • 调试:如果你在开发过程中遇到渲染过于频繁或者数据依赖错误的问题,renderTracked 可以帮助你检查哪些数据触发了更新。你可以通过 console.log 输出依赖关系来诊断问题。

  • 理解 Vue 响应式系统:renderTracked 是理解 Vue 3 响应式系统内部工作原理的好工具,帮助你深入了解如何进行依赖追踪。

renderTriggered

renderTriggeredVue 3 中的一个开发工具钩子,它用于追踪组件的重新渲染过程。当组件的响应式数据发生变化,并导致视图重新渲染时,renderTriggered 钩子会被触发。这个钩子对于调试和优化性能特别有用,尤其是在开发过程中帮助我们理解哪些响应式数据的变化触发了组件的重新渲染。

Vue 3 中的响应式系统依赖于一个 响应式对象,当你修改对象中的某些属性时,Vue 会通过 依赖追踪 来追踪哪些组件或计算属性依赖于这个数据。然后,当这个数据发生变化时,Vue 会自动重新渲染这些组件。

renderTriggered 就是当这些重新渲染发生时,Vue 会触发一个事件,告诉我们哪些数据的变化导致了视图的更新。

renderTriggered 用法:

renderTriggeredVue 3 的一个生命周期钩子函数,用于在组件渲染时触发。你可以使用它来观察组件视图的更新以及触发重新渲染的具体原因。

import { reactive, effect, renderTriggered } from 'vue';

const state = reactive({
  count: 0
});

effect(() => {
  console.log(state.count);  // 当 state.count 变化时,重新渲染并触发 effect
});

renderTriggered(() => {
  console.log("Component re-rendered due to data change.");
});

state.count++;  // 触发视图更新,renderTriggered 会被调用

如何在开发中使用 renderTriggered

  • 调试性能:当组件频繁重新渲染时,renderTriggered 可以帮助你识别哪些数据变动导致了不必要的重渲染。你可以通过这个钩子来确定是否需要优化某些计算属性或方法,以减少不必要的渲染。

  • 性能优化:通过结合 renderTriggeredrenderTracked,你可以深入了解组件的渲染过程,优化响应式数据的管理,减少组件的不必要重渲染。

renderTracked 和 renderTriggered 的区别

  • renderTracked:该钩子会在响应式数据的 getter 被访问时触发,用来追踪响应式数据的依赖。

  • renderTriggered:该钩子会在响应式数据的 setter 被调用且引起组件重新渲染时触发,用来监控数据变化并触发渲染。

至此,与 Vue 组件的生命周期相关的内容都介绍完了。通过本节的学习,希望读者对生命周期有一个比较清楚的认识,知道每个生命周期钩子函数触发的时机,并且知道使用这些钩子函数可以执行哪些操作。总之,掌握好这些知识是学习 Vue 的基础。