对于 vue3 的生命周期,我们经常性会去疑问,生命周期有哪些呢,它是怎么去实现的, 又是什么时候调用的。
下面这个表格列出了所有选项式api生命周期钩子和组合式api生命周期钩子,以及他们的对应关系和执行的时机。
composition api | options api | 执行时机 |
---|---|---|
— | beforeCreate | 初始化组件内的属性(如:data,props,watch,computed等)之前 |
— | created | 初始化组件内的属性(如:data,props,watch,computed等)之后 |
onBeforeMount | beforeMount | 组件开始挂载之前 |
onMounted | mounted | 组件挂载之后 |
onBeforeUpdate | beforeUpdate | 组件数据更新之后,页面更新之前 |
onUpdated | updated | 组件数据更新之后,页面更新之后 |
onBeforeUnmount | beforeUnmount | 组件即将卸载,但还未卸载 |
onUnmounted | unmounted | 组件卸载之后 |
onErrorCaptured | errorCaptured | 捕获了后代组件传递的错误时 |
onRenderTracked | renderTracked | 响应式依赖被组件的渲染作用追踪后,仅开发模式下使用 |
onRenderTriggered | renderTriggered | 响应式依赖被组件触发了重新渲染之后,仅开发模式下使用 |
onActivated | activated | 组件被keep-alive包裹,页面从不活动状态变为活动状态执时 |
onDeactivated | deactivated | 组件被keep-alive包裹,页面从活动状态变为不活动状态执时 |
onServerPrefetch | serverPrefetch | 组件实例在服务器上被渲染之前,为异步函数,仅ssr模式使用 |
首先,我们可以根据 onBeforeMount、onMounted 的源码定义,可以看出定义钩子函数时是直接调用 injectHook 挂载在实例化上。
import { currentInstance, setCurrentInstance } from './component'
const enum LifecycleHooks {
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE)
export const onUpdated = createHook(LifecycleHooks.UPDATED)
export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT)
export const onUnmounted = createHook(LifecycleHooks.UNMOUNTED)
// 核心就是这个生命周期函数要和当前组件产生关联
function createHook (lifecycle) {
return function (hook, target = currentInstance) { // hook 自定义方法
// 获取到当前组件的实例和生命周期产生关联
injectHook(lifecycle, hook, target)
}
}
/**
*
* @param type bm、m、bu、u等代表生命周期的简写变量
* @param hook 自定义的方法
* @param target 当前 vue 实例
*/
function injectHook (type, hook, target) {
if (target) {
const hooks = target[type] || (target[type] = [])
const wrappedHook = () => {
setCurrentInstance(target)
hook()
setCurrentInstance(null)
}
hooks.push(wrappedHook) // 挂载的时候,直接放到对应的数组里面
}
}
这段代码的作用是定义了一系列组件生命周期钩子函数,并将传入的自定义钩子函数与当前组件实例关联起来。
首先,通过 injectHook 函数我们知道,每个生命周期钩子函数都是存放到实例化的变量里面,所以,要先从实例化里面取出。例如:
const { bm, m } = instance // 获取
if (bm) {
invokeArrayFns(bm)
}
其次,用户自定义的 hook 函数都是存放到对应的生命周期函数的数组中,所以调用的时候直接遍历数组执行即可。
const invokeArrayFns = (fns) => {
for (let i = 0; i < fns.length; i++) {
fns[i]()
}
}
从之前的篇文章中我们知道 createApp
函数最为重要的一个函数是 mount
,在 mount
函数中会调用mountComponent
经行组件的挂载。我们可以从 mountComponent
函数为口入逐步分析生命周期执行的过程。
mountComponent 函数用于挂载组件类型的虚拟 DOM 节点。
函数的作用是创建组件实例、解析组件实例对象并设置渲染效果。
const mountComponent = (initialVNode, container) => {
const instance = initialVNode.component = createComponentInstance(
initialVNode)
setupComponent(instance) // 解析组件的实例对象
setupRenderEffect(instance, container)
}
初始化一个组件的实例对象,添加相关属性(vnode、type、props、attrs、ctx、proxy)。
const createComponentInstance = (vnode) => {
const instance = {
vnode,
type: vnode.type, // 组件的类型
props: {}, // 组件的属性
attrs: {},
setupState: {}, // setup返回值
ctx: {}, // proxy.props.name -> proxy.name
proxy: {},
render: false,
isMounted: false, // 是否挂载
children: []
}
instance.ctx = { _: instance } // 实例上下文
return instance
}
setupRenderEffect 创建一个 effect,用于在组件状态变化时触发重新渲染。
该 effect 依赖于组件实例的响应式数据,当响应式数据发生变化时,会触发该 effect,进而调用组件实例的 render 方法重新渲染组件,并将渲染结果插入到 container 容器中。
import { effect } from '@vue/reactivity'
const setupRenderEffect = (instance, container) => {
// 创建响应式的副作用渲染函数
effect(function componentsEffect () {
// 判断第一次加载
// 执行render,获取方法中的依赖收集effect,属性改变再次执行
if (!instance.isMounted) {
// beforeMount hook
const { bm, m } = instance
if (bm) {
invokeArrayFns(bm)
}
const proxyToUse = instance.proxy // 组件的实例
const subTree = instance.subTree = instance.render.call(proxyToUse,
proxyToUse) // 渲染组件生成子树vnode
// 将子树vnode挂载到container上
patch(null, subTree, container)
instance.isMounted = true
// 渲染完成 mounted hook
if (m) {
invokeArrayFns(m)
}
} else {
const { u, bu } = instance
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
const proxyToUse = instance.proxy // 组件的实例
const prevTree = instance.subTree // 保留渲染生成的子树根DOM节点
const nextTree = instance.render.call(proxyToUse, proxyToUse) // 获取子组件树
instance.subTree = nextTree // 更新当前的子组件树
patch(prevTree, nextTree, container) // 渲染新的子组件树到容器中
// update hook
if (u) {
invokeArrayFns(u)
}
}
})
}
在执行组件挂载之前,会检测组件实例上是否存在注册的 beforeMount
钩子函数(bm)。
如果存在,通过遍历 instance.bm
数组并使用 invokeArrayFns
方法依次执行这些钩子函数。
这样设计的原因是用户可以通过多次调用 onBeforeMount
函数来注册多个 beforeMount
钩子函数,保证它们按注册顺序依次执行。
在之前分析 patch 函数的执行流程中我们知道,当存在旧节点并且新旧节点的类型不同时,会先卸载旧节点,然后进行新节点的渲染和挂载。用户也可以手动调用 unmount 函数经行卸载。
unmount 函数的作用是执行组件的卸载操作。
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
unmount 函数的作用包括以下几个方面:
从父组件中移除:unmount 函数会将组件从其父组件中移除。这意味着组件将不再在父组件的模板中渲染,并且在父组件的组件实例中,该组件的相关引用将被清除。
卸载组件实例:unmount 函数会执行组件实例的卸载过程。在卸载过程中,vue 3 会依次触发生命周期钩子函数,包括 onBeforeUnmount 和 unmounted。你可以在这些钩子函数中执行一些清理操作或释放资源的操作。
移除 DOM 元素:unmount 函数会将组件的根 DOM 元素从 DOM 树中移除,以及解绑组件与其根 DOM 元素之间的关联。这样,组件的模板内容将不再显示在页面上,并且与 DOM 相关的事件监听器和其他绑定也将被清除。
在 unmount 函数中如果 ShapeFlags 类型是 COMPONENT 的话会执行 unmountComponent 函数经行组件的卸载。
unmountComponent 函数的作用是卸载组件实例。为了便于理解,以下代码为简化之后的代码:
const unmountComponent = (
instance,
parentSuspense,
doRemove,
) => {
const { bum, um } = instance
// beforeUnmount hook
if (bum) {
invokeArrayFns(bum)
}
// stop effects in component scope
scope.stop()
// unmounted hook
if (um) {
queuePostRenderEffect(um, parentSuspense) // 这里会循环 um 里面的函数
}
}
希望通过本篇文章可以帮助读者更好的了解生命周期的执行过程。