Vue生命周期解析
1. 从Vue 实例说起
1.1 JavaScript 类型与实例
本来是没有这节的。由于考虑到Vue3相对于Vue2变化中的一些细节,如到底什么是Vue的实例,特意补了本节为后文铺垫。
ES6后,JavaScript中,有6种所谓基本类型,即null
表示空值类型、 undefined
表示未定义值类型、boolean
表示布尔值类型、number
表示数值类型、string
表示字符串值类型、symbol
表示唯一符号值类型。这些类型加上object
表示的对象类型,共同构成了如今JavaScript中的7种内置类型。其中对象是用来描述万物的, 对应的对象类型也是唯一不是基本类型的内置类型,因为每一个对象实例都可能由各种其它类型以复杂或者简单的关系符合而成。
在 JavaScript 中,实例对象是由 构造器函数 创建的。在一些基于类的语言中类名和其构造方法名是一致的,JavaScript不是基于类的面向对象语言但可以类比之。比如Array
表示数组,数组是一类具有特定行为和特征的通称,即相当于强面向对象语言中的类,与其相同的是 数组的构造器函数也写做Array
,只不过它是一个函数:Array()
,如果你感兴趣也可以类比称之为“构造方法”,但 JavaScript 其实始终中没有方法的概念,它真的就是函数。
构造函数是一个特殊的函数,在博文《JavaScript new 的原理与实现》 中讨论过构造函数与普通函数的不同,感兴趣的读者可以参考之。 在 JavaScript 中没有任何强制性的规定说构造函数一定要使用大峰驼命名的法则,只是 JavaScript 确实是连名字都是从 Java 抄过来的语言,因此约定熟成继承了这一不成文的规定,被用于构造函数的函数保持和对应的对象(类)一致,且都用大峰驼法命名。可想毫无悬念,JavaScript内置的对构造函数都时如此命名。如Array
的Array()
构造函数、Object
的Object()
构造函数。
1.2 Vue 的实例
需要注意,VUE实例是由构造函数Vue()
在new
作用下创建的Vue 对象的实例
,这个函数在VUE2中给出,然而在VUE3中没有提供Vue
对象的构造函数,而是一个应用创建函数creatApp()
,这有本质不同,首先对比在用法上:
前者使用了new
语法,因此Vue
是一个被用作构造函数的函数,一般在JavaScript中构造函数返回的是与构造函数同名的对象的实例,即Vue对象。这也就是我们说所谓创建Vue对象实例的原因。
但是后者就没有提供Vue对象,就也是说Vue3 中不支持直接使用Vue构造函数来创建Vue对象的实例。事实上creatApp
也没有返回给你Vue对象的实例!
1.2.1 Vue2 中的 Vue对象 实例
1) Vue2 创建Vue对象实例
Vue 框架为使用者提供一个 Vue
对象,在 Vue2 中为我们暴露了一个Vue
的构造函数(Vue()
),你需要先导入该对象的构造函数:
import Vue from 'vue' // 导入 Vue 构造函数 import App from './App.vue' // 根组件文件
接着通过这个函数创建一个Vue对象的实例:
// 通过构造器函数 Vue() 来创建 Vue 对象的实例 let app = new Vue({ render: h => h(App), })
所创建的app
就是一个Vue实例,如图:
2) Vue 对象的接口
从使用角度看,要了解Vue的实例对象怎么使用,最快的方式就是查看Vue的接口文档。Vue 的接口如下:
export interface Vue { readonly $el: Element; readonly $options: ComponentOptions<Vue>; readonly $parent: Vue; readonly $root: Vue; readonly $children: Vue[]; readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined }; readonly $slots: { [key: string]: VNode[] | undefined }; readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined }; readonly $isServer: boolean; readonly $data: Record<string, any>; readonly $props: Record<string, any>; readonly $ssrContext: any; readonly $vnode: VNode; readonly $attrs: Record<string, string>; readonly $listeners: Record<string, Function | Function[]>; $mount(elementOrSelector?: Element | string, hydrating?: boolean): this; $forceUpdate(): void; $destroy(): void; $set: typeof Vue.set; $delete: typeof Vue.delete; $watch( expOrFn: string, callback: (this: this, n: any, o: any) => void, options?: WatchOptions ): (() => void); $watch<T>( expOrFn: (this: this) => T, callback: (this: this, n: T, o: T) => void, options?: WatchOptions ): (() => void); $on(event: string | string[], callback: Function): this; $once(event: string | string[], callback: Function): this; $off(event?: string | string[], callback?: Function): this; $emit(event: string, ...args: any[]): this; $nextTick(callback: (this: this) => void): void; $nextTick(): Promise<void>; $createElement: CreateElement; }
3) Vue 实例对象的作用
①. 通过传入选项个性化 Vue 实例对象功能
在Vue2中,当创建一个 Vue 实例时,你可以传入一个选项对象。这个选项对象的内容包括如:
- 数据项 (
data
、props
、propsData
、computed
、methods
、watch
)、 - DOM项(
el
、template
、render
、renderError
)、 - 生命周期钩子项(见 Vue2 生命周期 部分)、
- 资源项(
directives
、filters
、components
)、 - 组合项(
parent
、mixins
、extends
、provide / inject
) - 以及其他项(如
name
、delimiters
、functional
、model
、inheritAttrs
、comments
)
②. 通过 Vue 对象传递参数
在 Vue2 组件中 this
指向 Vue 对象的实例,这也就意味着你可以通过向 Vue 的原型上挂载属性和函数以实现在不同组件中进行通信。
1.2.2 Vue3 中的 App
1) Vue3 的 createApp()
函数
与Vue2中不一样的是,Vue3不再直接提供Vue()
构造函数,取而代之的是createApp()
函数,对应的,写法也更改成了:
import { createApp } from 'vue' import App from './App.vue' // 根组件文件 let app = createApp(App) app.mount('#app')
这里的app
是什么,还是Vue的实例么?很抱歉不是!在我的另一篇博文博文《JavaScript new 的原理与实现》中讲过,对象的实例是通过new 构造函数()
形式语法创建的,而不适用new
时只是一个普通函数。在这里这个普通函数也的确不是我们所期待的vue实例:
2) Vue3 的 defineComponent()
函数
我们需要一个其它的方案来像 ②. 通过 Vue 对象传递参数 中一样个性化定义一个 Vue 组件,事实上只要你愿意,可以像createApp(App)
在Vue3中,提供 defineComponent()
函数 在定义 Vue 组件时提供类型提示的帮助,这能更好地使用TypeScript作为开发工具。
2. 什么是vue生命周期
vue生命周期
指 vue 实例生命周期钩子
。它是 在vue实例从创建到销毁过程中的各个阶段自动地被调用的、 以属性形式声明的 一组 函数
。为了方便再组件的不同阶段加入相应的定制功能,对于一个普通的组件,我们对Vue实例生命阶段定义若干个个关键时间点,这些时间点都会安装相应的名称自动地在对应时刻调用其对应的函数,也就是说相关的函数执行顺序和其调用顺序一致,就像一到某个时刻一段功能就被钩起,因此称作生命周期钩子。
3. Vue2 生命周期
3.1 概述
在Vue2中,这些时间关键点分别为 Create、Mount、Update 和 Destroy。这里要指出的是,不论是创建、挂载,还是更新、销毁,都是需要时间的,也就是他们都是一个时间段。由于这些过程都是Vue框架为我们完成的,也不需要我们进行处理,因此我们完全可以将这几个过程认作时间点。具体而言,这四个点的功能和特点如下:
- 【Create】: Vue实例创建。
- 【Mount】 : Vue实例挂载。
- 【Update】: Vue实例数据更新。
- 【Destroy】: Vue实例销毁。
还有一些特殊的生命周期钩子,说他们特殊是因为他们用于特定的场景。如activated
、deactivated
和errorCaptured
。这一部分将在 3.3节 中讲述
3.2 图解 Vue 2生命周期
3.2.1 beforeCreate()
在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
3.2.2 created()
在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。
3.2.3 beforeMount()
在挂载开始之前被调用:相关的 render 函数首次被调用。
该钩子在服务器端渲染期间不被调用。
3.2.4 mounted()
实例被挂载后调用,这时 el 被新创建的 vm.e l 替换了。如果根实例挂载到了一个文档内的元素上,当 m o u n t e d 被调用时 v m . el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.el替换了。如果根实例挂载到了一个文档内的元素上,当mounted被调用时vm.el 也在文档内。
注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick:
mounted: function () { this.$nextTick(function () { // 仅在整个视图都被渲染之后才会运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
3.2.5 beforeUpdate()
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行。
3.2.6 updated()
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。
注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等到整个视图都渲染完毕,可以在 updated 里使用 vm.$nextTick:
updated: function () { this.$nextTick(function () { // 仅在整个视图都被重新渲染之后才会运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
3.2.7 beforeDestroy()
实例销毁之前调用。在这一步,实例仍然完全可用。
该钩子在服务器端渲染期间不被调用。
3.2.8 destroyed()
实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
该钩子在服务器端渲染期间不被调用。
3.3 几个特殊的生命周期钩子
3.3.1 activated 和 deactivated 钩子
这两个钩子分别在被 keep-alive 组件
缓存的组件激活时 和 被 keep-alive 组件
缓存的组件失活时 调用。在我的另外一篇博文《vue3 中使用动画技术》中介绍了 transition
和 keep-alive
两个Vue的内置组件。当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行,并且在 Vue 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive>
树内的所有嵌套组件中触发。这主要用于保留组件状态或避免重新渲染。
可见要讲清楚 activated()
和 deactivated()
这两个钩子函数的作用就必须先和大家介绍 keep-alive 组件
是一个什么杨的内置组件。类似于 HTTP 协议中 KeepAlive 允许多个请求或者响应共用一个TCP连接以避免连接频繁地销毁和创建,Veu 中内置的 keepalive
组件的作用是避免一个组件被频繁的销毁和创建。
3.3.2 errorCaptured 钩子
该钩子在捕获一个来自后代组件的错误时被调用。
4. Vue3 生命周期
4.1 概述
Vue3中,有两种风格的生命周期钩子API形式。
一种是选项式API,它对应的生明周期函数称作和Vue2中的生命周期钩子形式上类似,但叫做 生命周期选项
。其命名方式沿用了 vue2 中的命名方式,即名称中不带有on
,如beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeUnmount
、unmounted
、…
另外外一种是所谓的组合式 API,对应的生命周期函数称作生命周期钩子
,这种形式都应该 在组件的 setup()
阶段被同步调用,其命名特点是,名称以on
作为开头。
4.2 图解 Vue3 生命周期
下图以选项式API 为例介绍Vue3生命周期的过程:
4.2.1 beforeCreate()
在组件实例初始化完成之后立即调用。
interface ComponentOptions { beforeCreate?(this: ComponentPublicInstance): void }
会在实例初始化完成、prop 解析之后、data() 和 computed 等选项处理之前立即调用。
注意,组合式 API 中的 setup() 钩子会在任何选项式 API 钩子之前调用, beforeCreate() 也不例外。
4.2.2 created()
在组件实例处理完所有与状态相关的选项后调用。
interface ComponentOptions { created?(this: ComponentPublicInstance): void }
当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。然而,此时挂载阶段还未开始,因此 $el property 仍不可用。
4.2.3 beforeMount()
在组件被挂载之前调用。
interface ComponentOptions { beforeMount?(this: ComponentPublicInstance): void }
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
这个钩子在服务端渲染时不会被调用。
4.2.4 mounted()
在组件被挂载之后调用。
interface ComponentOptions { mounted?(this: ComponentPublicInstance): void }
组件在以下情况下被视为已挂载:
所有同步子组件都已经被挂载。(不包含异步组件或 树内的组件)
其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。
这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于约束给客户端的 DOM 相关代码。
这个钩子在服务端渲染时不会被调用。
4.2.5 beforeUpdate()
在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
interface ComponentOptions { beforeUpdate?(this: ComponentPublicInstance): void }
这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。
这个钩子在服务端渲染时不会被调用。
4.2.6 updated
在组件即将因为一个响应式状态变更而更新其 DOM 树之后调用。
interface ComponentOptions { updated?(this: ComponentPublicInstance): void }
父组件的更新钩子将在其子组件的更新钩子之后调用。
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
这个钩子在服务端渲染时不会被调用。
WARNING
不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!
4.2.7 beforeUnmount()
在一个组件实例被卸载之前调用。
interface ComponentOptions { beforeUnmount?(this: ComponentPublicInstance): void }
当这个钩子被调用时,组件实例依然还保有全部的功能。
这个钩子在服务端渲染时不会被调用。
4.2.8 unmounted()
在一个组件实例被卸载之后调用。
interface ComponentOptions { unmounted?(this: ComponentPublicInstance): void }
一个组件在以下情况下被视为已卸载:
其所有子组件都已经被卸载。
所有相关的响应式作用(渲染作用以及 setup() 时创建的计算属性和侦听器)都已经停止。
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
这个钩子在服务端渲染时不会被调用。
4.3 几个特殊的生命周期选项
4.3.1 activated 和 deactivated
和Vue2中activated 和 deactivated类似,参见 3.3.1 activated 和 deactivated 钩子。
4.3.2 errorCaptured
用于在捕获了后代组件传递的错误时调用。
4.4 Vue3 的组合式API
4.4.1 概述
以上章节我们都是依据 选项式 API 进行介绍的,这些关于生命周期的选项式API也称之为 生命周期选项。相对应的,也是 Vue3 的一大亮点,是它新增了 组合式API,在组合式API中,也对应的提供了生命周期相关的函数,称之为生命周期钩子。
4.4.2 setup()
4.4.2.1 概述
在 Vue3 中,新增的设置函数 setup()
用于在生命周期前配置相关数据,它和生命周期钩子一样分为 选项式 和 组合式 两种写法,只不过在Vue3 中更推荐的是 组合式 用法。
setup() 这个钩子在以下情况下,作为组件中使用组合式 API 的入口:
- 不搭配构建步骤使用组合式 API。
- 在 选项式 API 组件 中集成基于 组合式 API 的代码。
4.4.2.2 返回响应式数据
从 setup 返回 ref
时,它会自动 浅层
解包,因此你无须再在模板中为它写 .value。当通过 this 访问时也会同样如此解包。
从 setup 返回 reactive
,响应式转换是深层
的:它会影响到所有嵌套的 property。一个响应式对象也将深层地解包任何 ref property,同时保持响应性。值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。若要避免深层响应式转换,只想保留对这个对象顶层次访问的响应性,应用 shallowReactive() 作替代。
4.4.2.3 选项式API的 Setup 示范
这和 vue2 中看起来是基本没有太大区别的:
<template> <button @click="count++">{{ count }}</button> </template> <script> import { ref } from 'vue' export default { setup() { const count = ref(0) // 返回值会暴露给模板和其他的选项式 API 钩子 return { count } }, } </script>
不过现在我们更倾向于使用 TypeScript 来进行开发,因此还需要多了解一些东西,那就是defineComponent()
函数。在Vue3 中提供了defineComponent()
函数,他是在定义 Vue 组件时提供类型提示的帮助函数:
function defineComponent( component: ComponentOptions | ComponentOptions['setup'] ): ComponentConstructor
其中:
第一个参数是一个组件选项对象。返回值将是相同的选项对象,因为该函数本质上在运行时没有任何操作,仅用于类型推断。
注意返回值的类型有一点特别:它会是一个构造函数类型,它的实例类型是根据选项推断出的组件实例类型。这是为该返回值在 TSX 中用作标签时提供类型推断支持。
你可以像这样从 defineComponent() 的返回类型中提取出一个组件的实例类型 (与其选项中的 this 的类型等价):
const Foo = defineComponent(/* ... */) type FooInstance = InstanceType<typeof Foo>
4.4.2.4 组合式API的 Setup 示范
组合式API 更多和<script setup>
语法糖一起使用,用起来更为方便。<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时则推荐该语法。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 Typescript 声明
prop
和抛出事件。 - 更好的运行时性能 (其模板会被编译成同一作用域的渲染函数,没有任何的中间代理)。
- 更好的 IDE 类型推断性能 (减少语言服务器从代码中抽离类型的工作)。
要使用这个语法,需要将 setup attribute 添加到 <script>
代码块上,例如:
<template> <button @click="count++">{{ count }}</button> </template> <script setup> import { ref } from 'vue' const count = ref(0) </script>
5. Vue2 与 Vue3 生命周期的对比
5.1 生命周期形式变化
在 Vue2 中生命周期函数以 选项式API 提供,生命周期选项即生命周期钩子;
而在 Vue3 中生命周期函数以两种形式提供,选项式API也称作生命周期选项,其用法与 Vue2 一样。组合式API也称生命周期钩子
5.2 生命周期内容变化
5.2.1 一般生命周期钩子
Vue2生命周期函数 | 描述 | Vue3生命周期选项 | 描述 | Vue3 生命周期钩子 | 描述 |
beforeCreate |
在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。 | beforeCreate |
在组件实例初始化完成之后立即调用 | 不需要 | - |
created |
在实例创建完成后被立即同步调用。 | created |
在组件实例处理完所有与状态相关的选项后调用 | 不需要 | - |
beforeMount |
在挂载开始之前被调用 相关的 render 函数首次被调用。 |
beforeMount |
在组件被挂载之前调用 | onBeforeMount() |
注册一个钩子,在组件被挂载之前被调用 |
mounted |
实例被挂载后调用 这时 el 被新创建的 vm.$el 替换了。 |
mounted |
在组件被挂载之后调用 | onMounted() |
注册一个回调函数,在组件挂载完成后执行 |
beforeUpdate |
在数据发生改变后,DOM 被更新之前被调用。 | beforeUpdate |
在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用 | onBeforeUpdate() |
注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用 |
updated |
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。 | updated |
在组件即将因为一个响应式状态变更而更新其 DOM 树之后调用 | onUpdated() |
注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用 |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。 | beforeUnmount | 在一个组件实例被卸载之前调用 | onBeforeUnmount() | 注册一个钩子,在组件实例被卸载之前调用 |
destroyed | 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。 | unmounted | 在一个组件实例被卸载之后调用 | onUnmounted() | 注册一个回调函数,在组件实例被卸载之后调用 |
5.2.2 特殊生命周期钩子
Vue2生命周期函数 | 描述 | Vue3生命周期选项 | 描述 | Vue3 生命周期钩子 | 描述 |
activated |
若组件实例是 缓存树的一部分,当组件被插入到 DOM 中时调用 | activated |
被 keep-alive 缓存的组件激活时调用 | onActivated() |
注册一个回调函数,若组件实例是 缓存树的一部分,当组件被插入到 DOM 中时调用 |
deactivated |
若组件实例是 缓存树的一部分,当组件从 DOM 中被移除时调用 | deactivated |
被 keep-alive 缓存的组件失活时调用 | onDeactivated() |
注册一个回调函数,若组件实例是 缓存树的一部分,当组件从 DOM 中被移除时调用 |
errorCaptured |
在捕获了后代组件传递的错误时调用 | errorCaptured |
在捕获一个来自后代组件的错误时被调用 | onErrorCaptured() |
注册一个钩子,在捕获了后代组件传递的错误时调用 |
5.2.3 新增仅用于服务端渲染(SSR)的钩子
Vue2生命周期函数 | 描述 | Vue3生命周期选项 | 描述 | Vue3 生命周期钩子 | 描述 |
无 | - | serverPrefetch | 当组件实例在服务器上被渲染之前要完成的异步函数 | onServerPrefetch() | 注册一个异步函数,在组件实例在服务器上被渲染之前调用。 |
5.2.4 新增仅用于开发阶段的钩子
Vue2生命周期函数 | 描述 | Vue3生命周期选项 | 描述 | Vue3 生命周期钩子 | 描述 |
无 | - | renderTracked | 在一个响应式依赖被组件的渲染作用追踪后调用 | onRenderTracked() | 注册一个调试钩子,当响应式依赖被组件的渲染作用追踪后调用 |
无 | - | renderTriggered | 在一个响应式依赖被组件触发了重新渲染之后调用 | onRenderTriggered() | 注册一个调试钩子,当响应式依赖触发了组件渲染作用的运行之后调用 |