vue3源码学习8-生命周期
Vue.js 3.0映射Vue.js 2.x:
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy-> onBeforeUnmount
destroyed -> onUnmounted
activated -> onActivated
deactivated -> onDeactivated
errorCaptured -> onErrorCaptured
3.0新增 -> onRenderTracked
3.0新增 -> onRenderTriggered
下面看3.0的生命周期钩子函数的实现packages/runtime-core/src/apiLifecycle.ts:
// 枚举值: packages/runtime-core/src/component.ts
export const enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
DEACTIVATED = 'da',
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp'
}
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
)
export function onErrorCaptured<TError = Error>(
hook: ErrorCapturedHook<TError>,
target: ComponentInternalInstance | null = currentInstance
) {
injectHook(LifecycleHooks.ERROR_CAPTURED, hook, target)
}
可以发现,除了onErrorCaptured,其他的都是通过createHook函数创建的,通过传入不同的字符串参数,表示不同的钩子函数。然后看createHook函数:
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
createHook会返回一个函数,内部通过injectHook注册钩子函数,这里不直接使用injectHook API,而是用createHook做了一层封装,是因为钩子函数内部执行逻辑都很类似,只有第一个参数字符串不同,这样的代码可以进一步封装,典型的函数柯里化技巧。
而且调用createHook返回的函数时,不再需要传入lifecycle字符串,因为执行createHook函数时已经实现了该参数的保留,所以当我们通过onMounted(hook)注册一个钩子函数时,内部就通过injectHook('m', hook)去注册,接下来看injectHook函数的实现:
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
const hooks = target[type] || (target[type] = [])
// cache the error handling wrapper for injected hooks so the same hook
// can be properly deduped by the scheduler. "__weh" stands for "with error
// handling".
// 封装 hook 钩子函数并缓存
const wrappedHook =
hook.__weh ||
(hook.__weh = (...args: unknown[]) => {
if (target.isUnmounted) {
return
}
// disable tracking inside all lifecycle hooks
// since they can potentially be called inside effects.
// 执行wrappedHook钩子函数时,会先停止依赖收集,因为钩子函数内部访问的响应式对象,
// 通常已经执行过依赖收集,所以钩子函数执行的时候没必要再次依赖收集。
pauseTracking()
// Set currentInstance during hook invocation.
// This assumes the hook does not synchronously trigger other hooks, which
// can only be false when the user does something really funky.
// 设置target为当前组件实例,为了确保此时的currentInstance和注册注册钩子函数时一致,
// 会通过setCurrentInstance(target)设置target为当前组件实例
setCurrentInstance(target)
// 通过callWithAsyncErrorHandling执行注册的hook钩子函数
const res = callWithAsyncErrorHandling(hook, target, type, args)
// 函数执行完毕后设置当前运行组件实例为null
unsetCurrentInstance()
// 恢复依赖收集
resetTracking()
return res
})
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
}
}
该函数主要是对用户注册的钩子函数做了一层封装,然后添加到一个数组中,把数组保存在当前实例的target上,这里type是用来区分钩子函数的字符串。比如,onMounted注册的钩子函数在组件实例上就是通过instance.m来保存,hooks的保存逻辑我们可以用以下代码说明:
arr = obj['aa'] = []
// 输出[]
arr.push(1)
// 输出1
arr.push(2)
// 输出2
obj.aa
// 输出(2) [1, 2]
这样设计是因为声明周期的钩子函数是在声明周期的各个阶段执行的,所以钩子函数必须保存在当前实例上,这后面就可以在组件实例上通过不同的字符串找到对应的钩子函数并执行。对于相同的钩子函数,会把封装的wrappedHook钩子函数缓存到hook.__weh中,后续通过scheduler方式执行的钩子函数就会被去重。
下面看一下onBeforeMount和onMounted,onBeforeMount注册的beforeMount钩子函数会在组件挂载前执行,onMounted注册的mounted钩子函数会在组件挂载之后执行,回忆一下之前组件副作用渲染函数关于组件挂载的部分packages/runtime-core/src/renderer.ts:
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 获取组件实例上通过onBeforeMount 钩子函数和 onMounted 注册的钩子函数
const { bm, m, parent } = instance
// 执行beforeMounted钩子函数
if (bm) {
// 因为用户可以通过多次执行onBeforeMount函数注册多个beforeMount钩子函数,所以instance.bm是一个数组,
// 通过遍历这个数组依次执行beforeMount钩子函数
invokeArrayFns(bm)
}
// 渲染组件生成子树vnode
const subTree = (instance.subTree = renderComponentRoot(instance))
// 把子树vnode挂载到container
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// 保留渲染生成的子树根DOM节点
initialVNode.el = subTree.el
// 执行mounted钩子函数
if (m) {
// 把mounted钩子函数推入pendingPostFlushCbs中,然后在整个应用render完成后,同步执行pendingPostFlushCbs
// 函数调用mounted钩子函数
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
} else {
// 更新组件
}
}
}
接口调用应该放在哪个声明周期?3.0用setup代替了2.x的beforeCreate和created钩子函数,所以组件初始化的异步请求可以放在setup函数、beforeMount钩子函数或者mounted钩子函数,语义化角度来说,推荐放在setup函数中执行。如果想依赖DOM去做一些初始化,就只能放在mounted钩子函数中,这样才能拿到组件渲染后的DOM。
对于嵌套组件,组件在顾炎在相关生命周期钩子函数时,先执行父组件的beforeMount,然后是子组件的beforeMount,接着是子组件的mounted,最后执行父组件的mounted。
接下来看onBeforeUpdate和onUpdated,onBeforeUpdate注册的beforeUpdate钩子函数会在组件更新之前执行,onUpdated注册的updated钩子函数会在组件更新之后执行,接着看上面组件副作用渲染函数关于组件更新的部分:
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 挂载组件
} else {
// 更新组件
let { next, bu, u, parent, vnode } = instance
// next表示新的组件vnode
if (next) {
next.el = vnode.el
// 更新组件vnode节点信息
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// 执行beforeUpdate钩子函数
if (bu) {
invokeArrayFns(bu)
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树
instance.subTree = nextTree
// 组件更新核心逻辑,根据新旧子树 vnode 做patch
patch(
prevTree,
nextTree,
// 如果在teleport组件中父节点可能已经改变,所以容器直接找到旧树DOM元素的父节点
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// 缓存更新后的DOM节点
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// 缓存更新后的DOM节点
next.el = nextTree.el
// 执行 updated钩子函数
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
}
}
}
在beforeUpdate钩子函数执行时,组件DOM还没更新,如果想在组件更新前访问DOM,例如手动移除已添加的事件监听器,可以注册在这个钩子函数。在updated钩子函数执行时,组件DOM已更新,所以现在可以执行依赖于DOM的操作。不要在updated钩子函数中更改数据,因为这样会再次触发组件更新,导致无限递归更新。可以使用计算属性或者watcher代替。
父组件更新不一定会导致子组件更新,因为vue的更新粒度是组件级别的。
接下来看onBeforeUnmount 和 onUnmounted,onBeforeUnmount注册的beforeUnmount钩子函数会在组件销毁之前执行,onUnmounted注册的unmounted钩子函数会在组件销毁之后执行:
// 主要就是清理组件实例上绑定的effects副作用函数和注册的副作用渲染函数update,已经调用unmounted销毁子树
const unmountComponent = (
instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null,
doRemove?: boolean
) => {
const { bum, scope, update, subTree, um } = instance
// 执行beforeUnmount钩子函数
// beforeUnmount hook
if (bum) {
invokeArrayFns(bum)
}
// stop effects in component scope
// 清理组件引用的effects副作用函数
scope.stop()
// update may be null if a component is unmounted before its async
// setup has resolved.
// 如果一个异步组件在加载前就销毁了,则不会注册副作用渲染函数
if (update) {
// so that scheduler will no longer invoke it
update.active = false
unmount(subTree, instance, parentSuspense, doRemove)
}
// unmounted hook
// 执行unmounted钩子函数
if (um) {
queuePostRenderEffect(um, parentSuspense)
}
}
unmount主要就是遍历子树,他会通过递归的方式来销毁子节点,遇到组件节点时执行unmountComponent,遇到普通节点时则删除DOM元素,组件的销毁过程和渲染过程类似,都是递归的过程。
对于嵌套组件,组件在执行销毁相关的生命周期钩子函数时,先执行父组件的beforeUnmount,再执行子组件的beforeUnmount,然后执行子组件的unmounted,最后执行父组件的unmounted。
接下来看onErrorCaptured注册的钩子函数,之前我们多次遇到一个方法callWithErrorHanding:
// packages/runtime-core/src/errorHandling.ts
// 执行一段函数并通过handleError处理错误
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[]
) {
let res
try {
res = args ? fn(...args) : fn()
} catch (err) {
handleError(err, instance, type)
}
return res
}
//
export function handleError(
err: unknown,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
throwInDev = true
) {
const contextVNode = instance ? instance.vnode : null
if (instance) {
let cur = instance.parent
// 为了兼容2.x版本,暴露组件实例给钩子函数
// the exposed instance is the render proxy to keep it consistent with 2.x
const exposedInstance = instance.proxy
// in production the hook receives only the error code
// 获取错误信息
const errorInfo = __DEV__ ? ErrorTypeStrings[type] : type
// 向上查找所有父组件是否有errorCaptured,有则执行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) === false
) {
return
}
}
}
// 遍历完当前组件实例的errorCaptured,错误没有得到正确的处理,继续向上查找父组件实例
cur = cur.parent
}
}
// 如果整个链路上都没有找到正确处理错误的errorCaptured钩子函数,往控制台输出未处理的错误
logError(err, type, contextVNode, throwInDev)
}
最后我们来看vue3.0新增的onRenderTracked和onRenderTriggered生命周期,它们是开发阶段渲染调试用的,依然来看副作用函数:
// packages/runtime-core/src/renderer.ts
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 挂载组件
} else {
// 更新组件
}
if (__DEV__) {
effect.onTrack = instance.rtc
? e => invokeArrayFns(instance.rtc!, e)
: void 0
effect.onTrigger = instance.rtg
? e => invokeArrayFns(instance.rtg!, e)
: void 0
update.ownerInstance = instance
}
}
}
可以看到onRenderTracked和onRenderTriggered分别在副作用渲染函数的onTrack和onTrigger对应的函数执行,先看onTrack函数:
packages/reactivity/src/effect.ts
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 执行一些依赖收集
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
// 执行onTrack函数,遍历执行注册的renderTracked钩子函数
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!
})
}
}
}
接着看trigger函数:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
triggerEffects(deps[0])
}
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
triggerEffect(effect, debuggerEventExtraInfo)
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
//执行onTrigger
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
// 执行effect
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!