vue3 源码之生命周期钩子
生命周期钩子对应的生命周期Option如下:
beforeCreate -> 使用 setup()
created -> 使用 use setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy-> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
errorCaptured -> onErrorCaptured
除此之外, 还新增了两个用于调试的生命周期:
- onRenderTracked
- onRenderTriggered
注册钩子函数
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)
export type DebuggerHook = (e: DebuggerEvent) => void
export const onRenderTriggered = createHook<DebuggerHook>(LifecycleHooks.RENDER_TRIGGERED)
export const onRenderTracked = createHook<DebuggerHook>(LifecycleHooks.RENDER_TRACKED)
createHook
的实现如下:
const createHook = function(lifecycle) {
return function (hook, target = currentInstance) {
injectHook(lifecycle, hook, target)
}
}
其内部的injectHook
实现如下:
function injectHook(type, hook, target = currentInstance, prepend = false) {
const hooks = target[type] || (target[type] = [])
// 封装 hook 钩子函数并缓存
const wrappedHook = hook.__weh ||
(hook.__weh = (...args) => {
if (target.isUnmounted) {
return
}
// 停止依赖收集
pauseTracking()
// 设置 target 为当前运行的组件实例
setCurrentInstance(target)
// 执行钩子函数
const res = callWithAsyncErrorHandling(hook, target, type, args)
setCurrentInstance(null)
// 恢复依赖收集
resetTracking()
return res
})
if (prepend) {
hooks.unshift(wrappedHook)
}
else {
hooks.push(wrappedHook)
}
}
该函数主要是对用户注册的钩子函数hook
做了一层封装, 然后添加到一个数组中, 把数组保存在当前组件实例的target上, 这里, key是用来区分钩子函数的字符串. 比如, onMounted
注册的钩子函数在组件实例上就是通过instance.m
来保存的.
组件实例在后续会通过不同的字符串key
找到对应的钩子函数数组并执行.
对于相同的钩子函数, 会把封装的wrappedHook
钩子函数缓存到hook.__weh
中, 这样后续通过scheduler
方式执行的钩子函数就会被去重.
后续执行wrappedHook
函数时, 会先停止依赖手机, 因为钩子函数内部访问的响应式DJUI想爱你个, 通常都已经执行过依赖手机, 所以钩子函数执行的时候就没有必要再次进行.
接着是设置 target 为当前组件实例.
在接下来通过callWithAsyncErrorHandling
方法执行注册的hook钩子函数, 执行完毕后设置当前运行组件实例为null, 并恢复依赖收集
onBeforeMount 和 onMounted
onBeforeMount
注册的beforeMount
钩子函数会在组件挂载之前执行
onMounted
注册的mounted
钩子会在组件挂载之后执行
我们回头看看组件渲染函数中关于组件挂载部分的实现:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 获取组件实例上通过 onBeforeMount 钩子函数和 onMounted 注册的钩子函数
const { bm, m } = instance;
// 渲染组件生成子树 vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 执行 beforemount 钩子函数
if (bm) {
invokeArrayFns(bm)
}
// 把子树 vnode 挂载到 container 中
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
// 保留渲染生成的子树根 DOM 节点
initialVNode.el = subTree.el
// 执行 mounted 钩子函数
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
}
else {
// 更新组件
}
}, prodEffectOptions)
}
在执行patch
挂载组件之前, 会检测组件实例上是否有注册的beforeMonut
钩子函数bm
, 如果有则通过invokeArrayFns
函数执行它, 挂载之后, 则检查是否有函数m
, 然后通过queuePostRenderEffect
将mounted
函数推入postFlushCbs
, 然后在整个树render以后, 同步执行flushPostFlushCbs
函数调用mounted
钩子函数
对于嵌套组件, 组件会先执行父组件的beforeMount, 然后子组件的beforeMount
, 然后子组件的mounted
, 最后是父组件的mounted
.
onBeforeUpdate 和 onUpdated
onBeforeUpdate注册的beforeUpdate
会在组件更新之前执行, onUpdated
会在组件更新之后执行.
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件
}
else {
// 更新组件
// 获取组件实例上通过 onBeforeUpdate 钩子函数和 onUpdated 注册的钩子函数
let { next, vnode, bu, u } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
}
else {
next = vnode
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树 vnode
instance.subTree = nextTree
// 执行 beforeUpdate 钩子函数
if (bu) {
invokeArrayFns(bu)
}
// 组件更新核心逻辑,根据新旧子树 vnode 做 patch
patch(prevTree, nextTree,
// 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
hostParentNode(prevTree.el),
// 缓存更新后的 DOM 节点
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG)
// 缓存更新后的 DOM 节点
next.el = nextTree.el
// 执行 updated 钩子函数
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}
}, prodEffectOptions)
}
不要在 updated 钩子函数中更改数据,因为这样会再次触发组件更新,导致无限递归更新
父组件的更新不一定会导致子组件的更新,因为 Vue.js 的更新粒度是组件级别的
onBeforeUnmount 和 onUnmounted
onBeforeUnmount 注册的 beforeUnMount 钩子函数会在组件销毁之前执行
onUnmounted 注册的 unmounted 钩子函数会在组件销毁之后执行
const unmountComponent = (instance, parentSuspense, doRemove) => {
const { bum, effects, update, subTree, um } = instance
// 执行 beforeUnmount 钩子函数
if (bum) {
invokeArrayFns(bum)
}
// 清理组件引用的 effects 副作用函数
if (effects) {
for (let i = 0; i < effects.length; i++) {
stop(effects[i])
}
}
// 如果一个异步组件在加载前就销毁了,则不会注册副作用渲染函数
if (update) {
stop(update)
// 调用 unmount 销毁子树
unmount(subTree, instance, parentSuspense, doRemove)
}
// 执行 unmounted 钩子函数
if (um) {
queuePostRenderEffect(um, parentSuspense)
}
}
组件销毁的逻辑比较简单, 就是清理组件实例上绑定的effect
副作用函数和注册的副作用渲染函数update
以及调用unmount
销毁子树.
#onErrorCaptured
组件销毁的逻辑比较简单, 就是清理组件实例上绑定的effect
副作用函数和注册的副作用渲染函数update
以及调用unmount
销毁子树.
我们在前面多次看到过一个函数callWithErrorHandling
, 他用于执行一段函数, 并通过handleError
处理错误. handleError
的实现如下:
function handleError(err, instance, type) {
const contextVNode = instance ? instance.vnode : null
if (instance) {
let cur = instance.parent
// 为了兼容 2.x 版本,暴露组件实例给钩子函数
const exposedInstance = instance.proxy
// 获取错误信息
const errorInfo = (process.env.NODE_ENV !== 'production') ? ErrorTypeStrings[type] : type
// 尝试向上查找所有父组件,执行 errorCaptured 钩子函数
while (cur) {
const errorCapturedHooks = cur.ec
if (errorCapturedHooks) {
for (let i = 0; i < errorCapturedHooks.length; i++) {
// 如果执行的 errorCaptured 钩子函数并返回 true,则停止向上查找。、
if (errorCapturedHooks[i](err, exposedInstance, errorInfo)) {
return
}
}
}
cur = cur.parent
}
}
// 往控制台输出未处理的错误
logError(err, type, contextVNode)
}
函数会从当前宝座的组件的父组件实例开始, 尝试去查找注册errorCaptured
钩子函数, 如果有则遍历判断errorCaptured
钩子函数是否为true
, 如果是则说明这个错误已经正确处理, 否则会通过logError
向控制台抛出错误.
所以errorCaptured
本质上用于捕获来自子孙组件的错误, 返回true就可以阻止错误继续向上传播
函数会从当前宝座的组件的父组件实例开始, 尝试去查找注册errorCaptured
钩子函数, 如果有则遍历判断errorCaptured
钩子函数是否为true
, 如果是则说明这个错误已经正确处理, 否则会通过logError
向控制台抛出错误.
所以errorCaptured
本质上用于捕获来自子孙组件的错误, 返回true就可以阻止错误继续向上传播
onRenderTracked 和 onRenderTriggered
onRenderTracked 和 onRenderTriggered 在开发阶段渲染调试用的
我们回顾一下创建副作用渲染函数的第二个参数, 在开发环境下他们是这样的:
onRenderTracked 状态跟踪
onRenderTracked
直译过来就是状态跟踪
,它会跟踪页面上所有响应式变量和方法的状态,也就是我们用return
返回去的值,它都会跟踪。只要页面有update
的情况,它就会跟踪,然后生成一个event
对象,我们通过event
对象来查找程序的问题所在。
使用onRenderTracked
同样要使用import
进行引入。
import { .... ,onRenderTracked,} from "vue";
引用后就可以在setup()
函数中进行引用了。
onRenderTracked((event) => {
console.log("状态跟踪组件----------->");
console.log(event);
});
写完后可以到终端中启动测试服务npm run serve
,然后看一下效果,在组件没有更新的时候onRenderTracked
是不会执行的,组件更新时,它会跟组里边每个值和方法的变化。
onRenderTriggered 状态触发
onRenderTriggered
直译过来是状态触发
,它不会跟踪每一个值,而是给你变化值的信息,并且新值和旧值都会给你明确的展示出来。
如果把onRenderTracked
比喻成散弹枪,每个值都进行跟踪,那onRenderTriggered
就是狙击枪,只精确跟踪发生变化的值,进行针对性调试。
使用它同样要先用import
进行引入
import { .... ,onRenderTriggered,} from "vue";
在使用onRenderTriggered
前,记得注释相应的onRenderTracked
代码,这样看起来会直观很多。 然后把onRenderTriggered()
函数,写在setup()
函数里边。
onRenderTriggered((event) => {
console.log("状态触发组件--------------->");
console.log(event);
});
对 event 对象属性的详细介绍:
- key 那边变量发生了变化
- newValue 更新后变量的值
- oldValue 更新前变量的值
- target 目前页面中的响应变量和函数
通过这些你能很好的对代码进行调试。这些调试用的钩子函数,如果你能正确合理的使用,是真的可以快速解决问题的。