Vue3.2 生命周期钩子函数简单分析
前言
大家好,我是咸鱼,今天的这篇文章让我们一起来探讨Vue3中生命周期函数的实现以及它是如何注入到Vue的执行流程中,在这篇文章中,可能会涉及到一些前面文章中的分析,如果没有看过前面的文章或者没有看过vue3源码的同学可以先去看看,好了那就让我们开始吧。
什么是生命周期钩子函数
在Vue3文档中是这样定义的:每一个Vue组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听、模板编译、挂载实例到DOM,以及数据改变时更新DOM,在此过程中,它也会允许被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
在vue中,有很多个生命周期钩子函数,需要注意的是要区分Vue2和Vue3的生命周期钩子,vue2是使用选项的语法,vue3是使用hook的语法,并且vue3没有created
和beforeCreate
(因为setup
的执行的事件非常早),在组件卸载时,vue2执行的是beforeDestroy
和destroyed
(需要开启兼容),vue3执行的是onBeforeUnmount
和onUnmount
。
vue2 | vue3 | |
---|---|---|
组件即将创建 | beforeCreate | -- |
组件创建完成 | created | -- |
组件即将挂载 | beforeMount | onBeforeMount |
组件挂载完成 | mounted | onMounted |
组件即将更新 | beforeUpdate | onbeforeUpdate |
组件更新完成 | updated | onUpdated |
组件即将卸载 | beforeDestroy | onBeforeUnmount |
组件已经卸载 | destroyed | onUnmount |
除了上述的这些生命周期钩子,还有一些其他的生命周期钩子函数,比如内置组件KeepAlive
的onActivate
和onDeactivated
(在vue2中是activated
和deactivated
选项),作用是DOM被移入或者移出时执行。剩下的生命周期钩子函数大家可以去文档中去查看。
生命周期钩子函数的实现
我们打开vue3源码的apiLifecycle.ts
文件,在里面我们可以清楚看到vue3实现的各个生命周期函数。
我们可以发现所有的onXXXX
都是调用了createHook
函数,这个函数它返回了一个函数,这个函数就是我们执行onXXX
的时候所执行的函数。
(伪代码)
(hook: T, target: ComponentInternalInstance | null = currentInstance) => {
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
}
这个函数有两个参数,第一个参数是需要在某一个特定时刻执行的函数hook
,第二个参数文档中没有提及,我们在开发中也不会使用,它等同于this
,也就是这个hook
函数是给谁注册的,并且在执行时当前实例也会设置为它。
我们继续往后看,这个函数执行了一个injectHook
函数,看函数的名字就知道它的作用,是将hook
函数注入到实例中的。
注入的方式很简单,首先每一个生命周期钩子函数都是可以注册一到多个,所以它们在实例上都是以数组的形成保存,数组里面都是在某特定时刻需要执行的函数。
injectHook
函数会先取出实例上对应的生命周期钩子函数的数组,如果没有默认给个空数组,包装hook
函数,并且缓存,最后放入生命周期钩子函数数组中即可。包装器也会返回。
// (伪代码)
const hooks = target[type] || (target[type] = [])
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
pauseTracking()
setCurrentInstance(target)
const res = callWithAsyncErrorHandling(hook, target, type, args)
unsetCurrentInstance()
resetTracking()
return res
})
hooks.push(wrappedHook)
return wrappedHook
经过上面的一系列步骤之后,在实例上的生命周期钩子函数数组中就会存在函数
vue3的生命周期钩子函数是怎么和vue2的生命周期选项合在一起的
自Vue3推出后,vue2的生命周期函数选项得到了保留,虽然有那么两个被废弃,但是大部分依旧是可以运行,vue3的一定比vue2的会先执行,这是因为处理顺序,正常情况下,vue3的生命周期函数会在setup()
中使用,而vue2的会在vue3的东西执行完毕后才会处理。
我们去看component.ts
中的applyOptions
函数,这个函数是用于处理vue2的内容,在里面我们看到一大堆的registerLifecycleHook
,这就把vue2的生命周期函数选项注入到实例中,
其实实现方法很简单,就是把写在vue2生命周期函数选项的函数拿到,然后通过vue3的方式去注入,我们来看registerLifecycleHook
的实现。
function registerLifecycleHook(
register: Function,
hook?: Function | Function[]
) {
if (isArray(hook)) {
hook.forEach(_hook => register(_hook.bind(publicThis)))
} else if (hook) {
register((hook as Function).bind(publicThis))
}
}
registerLifecycleHook
有两个参数,register
拿到的是我们正常使用的vue3的生命周期钩子,hook
就是写在生命周期选项中的函数,而把vue2的生命周期函数注入的方法就和我们自己手动调用vue3的生命周期钩子函数一样,唯一的区别在于需要更正函数的this
指向,指向当前全局组件实例代理对象(publicThis
是当前组件实例代理对象),而我们自己调用时当前全局组件实例代理对象就正确的目标对象。
特殊处理的生命周期钩子函数:activated
和destroyed
activated
当DOM是缓存树的一部分时,在移入时调用,destroyed
当DOM是缓存树的一部分,在移出时调用。
在所有的生命周期钩子函数中,有两个生命周期钩子函数需要特殊处理,它们是vue内置组件KeepAlive
的生命周期钩子函数。在正常情况下,和其他的生命周期钩子函数没有什么区别,但是在内置组件KeepAlive
嵌套调用之后,一切就有所不同了。
我们都知道,在组件和其后代都注入生命周期钩子函数时,如果组件产生更新,组件的对应的生命周期函数会执行,并且,其后代对应的生命周期函数也有可能会执行,而KeepAlive
的生命周期函数也不例外,当组件是缓存树的一部分,在移入和移出,如果其后代中存在KeepAlive
,那么其生命周期函数也会执行。
那么这里便存在一个问题,我们要如何很好的知道一个KeepAlive
中的ODM的后代中是否存在着KeepAlive
,遍历DOM? 或者是在数组中去追踪后代?不不不,这些都是比较耗费性能。不如我们变化另外一种思路,在一个组件注入activated
或destroyed
时,从组件的父链往上找,vue中的做法就是这样。
我们找到源码中的KeepAlive.ts
文件,可以看到onActivated
和onDestroyed
都是去调用了registerKeepAliveHook
函数,我们直接去看这个函数的实现。
// (伪代码)
const wrappedHook =
hook.__wdc ||
(hook.__wdc = () => {
// 仅当目标实例不在停用的分支中才触发钩子
let current: ComponentInternalInstance | null = target
// 跟着父链网上走,找外面的KeepAlive实例,如果实例上存在isDeactivated
// 说明vdnoe已经被卸载,不需要执行一次onDeactivate钩子函数
while (current) {
if (current.isDeactivated) {
return
}
current = current.parent
}
// 执行钩子函数
return hook()
})
}
injectHook(type, wrappedHook, target)
if (target) {
let current = target.parent
while (current && current.parent) {
if (isKeepAlive(current.parent.vnode)) {
injectToKeepAliveRoot(wrappedHook, type, target, current)
}
current = current.parent
}
}
它首先实现了一个分支停用检查包装器,并且缓存了这个包装器,方便后面调度正确的消除同一钩子函数的重复,接着就是正常的通过injectHook
注入到目标实例中。
这最后一步就很关键,先拿到目标实例的parent
(这里记作current
),只有current
和current
的parent
存在才会继续在父链上往上找,并且只有current
的parent
是KeepAlive
才会给current
注入钩子,可能这里有点不好理解,我们来看一个结构。
<div>
<KeepAlive> // 序号1
<Child1 />
<KeepAlive> // 序号2
<div>
<Child2 />
</div>
</KeepAlive>
</KeepAlive>
</div>
我们给Comp2
组件注册了一个activated
钩子函数,先拿到current
,也就是序号2的KeepAlive
,current
的parent
的序号1的KeepAlive
,这样current
和current.parent
存在与current.parent
是KeepAlive
两个条件成立,开始执行injectToKeepAliveRoot
函数。
下面我们就来看看injectToKeepAliveRoot
的实现,看给KeepAliveRoot
注入钩子函数和普通的注入钩子函数有啥区别,它的实现很简单,执行inject
函数,拿到返回的warpperHook
,然后立即给目标实例注入一个onUnmounted
钩子函数,方便在目标实例卸载时把在KeepAliveRoot
中的钩子函数数组中删除这个钩子函数。
function injectToKeepAliveRoot(hook, type, target, keepAliveRoot
) {
const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
onUnmounted(() => {
remove(keepAliveRoot[type]!, injected)
}, target)
}
其他的生命周期钩子函数
Vnode的生命周期钩子函数
在Vue源码中,有一种在文档中没有提及的生命周期钩子函数:Vnode生命周期函数
,vnode
也是有类似组件的生命周期,Vue中也提供了对应的生命周期钩子函数,比如onVnodeBeforeMount
、onVnodeBeforeUnmount
等等,但是没有created
阶段的钩子函数。
有一点需要注意,vnode的生命周期钩子函数注册的方式必须要放到props
中,比较常见的是组件给子组件去注册,又因为单向数据流,组件不能在自身注册,只能通过父组件注册。比如:<Comp onVnodeBeforeMount="fn" />
各个vnode生命周期钩子函数的执行时机如下:
什么时候会执行 | |
---|---|
onVnodeBeforeMount | 在vnode即将开始挂载时执行 |
onVnodeMounted | 在vnode挂载完成后执行 |
onVnodeBeforeUpdate | 在vnode即将开始更新时执行 |
onVnodeUpdated | 在vnode更新完毕后执行 |
onVnodeBeforeUnmount | 在vnode即将开始卸载时执行 |
onVnodeUnmounted | vnode卸载完成后执行 |
只要是对vnode进行挂载、更新、卸载的操作,都会去触发这几个生命周期函数,比如KeepAlive
内置组件,在移入的时候会触发onVnodeMounted
、移出时会触发onVnodeUnmounted
。
自定义指令生命周期函数
Vue允许用户自己定义自定义指令(Custom Directives),并且自定义指令的对象中可以包含几个生命周期钩子选项,这几个生命钩子选项和组件的生命周期选项比较类似,只有执行的时机不一样,具体可以去看文档中的 指令钩子中有详细说明,
我们主要来看他是如何处理的,打开文件directives.ts
,里面有两个函数:withDirectives
和invokeDirectiveHook
,一个是在负责处理旧指令时把指令加入到vnode
中,一个自定义指令生命周期钩子函数调用。
-
将指令加入到vnode中
这个没有什么好说的,就是单纯的解析指令的结构,然后将其加入到vnode的dir中
// 伪代码 function withDirectives(vnode, directives) { const internalInstance = currentRenderingInstance const instance = internalInstance.proxy // vnode中已经存在的指令 const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = []) for (let i = 0; i < directives.length; i++) { let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i] // 加入其中 bindings.push({ dir, instance, value, oldValue: void 0, arg, modifiers }) } return vnode }
-
自定义指令生命周期函数的执行
自定义指令生命周期函数的执行是通过一个函数去执行它,其实和事件调用者差不多,都是用一个函数去包裹要Fn
,然后去执行这个函数,然后在这个函数内部做一些处理。
// 伪代码
export function invokeDirectiveHook(vnode, prevVNode, instance, name) {
// 绑定的现指令
const bindings = vnode.dirs!
// 绑定的旧指令 会拿到之前的指令指定的value 会作为参数传递给指令函数
const oldBindings = prevVNode && prevVNode.dirs!
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
if (oldBindings) {
// 保留指令的oldValue
binding.oldValue = oldBindings[i].value
}
let hook = binding.dir[name] as DirectiveHook | DirectiveHook[] | undefined
if (hook) {
// 禁用生命周期钩子函数中的所有追踪
// 因为它们可能会被称为内部效果
pauseTracking()
callWithAsyncErrorHandling(hook, instance, ErrorCodes.DIRECTIVE_HOOK, [
vnode.el,
binding,
vnode,
prevVNode
])
resetTracking()
}
}
}
生命周期函数和调用系统的关系
在生命周期钩子函数中,有一部分是可以直接执行的,但是有一部分是的生命周期钩子函数是需要加入到post
队列中等待执行的。比如updated
生命周期钩子函数,这个生命周期钩子函数是需要等待更新完毕之后再执行。
最后以上仅为个人的分析,还是希望各位哥哥姐姐能指导指导。有说错或者遗漏的欢迎在评论区讲解,谢谢。
转载自:https://juejin.cn/post/7148630465065254948