foreword
When we use the Vue 3.0 Composition API, we usually use the lifecycle hooks function ( setup
onMounted
, onBeforeDestroy
, etc.) injection. Are there any restrictions when calling these APIs? The answer is yes. Let's take a look at the phenomenon through an example.
Comparison of injecting life cycle hooks at different stages
synchronization phase
This is the way we usually write life cycle injection. When the component is loaded, the console can print out mounted
normally.
<template>
<div />
</template>
<script lang="ts">
import { onMounted } from 'vue';
export default {
setup() {
onMounted(() => {
console.log('mounted');
});
},
};
</script>
asynchronous phase
What happens if we want to inject the lifecycle in the async phase on a whim?
export default {
setup() {
setTimeout(() => {
onMounted(() => {
console.log('mounted');
});
});
},
};
At this point we will find that the console outputs a Vue warning: [Vue warn]: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.
.
It probably means that when onMounted
is called, there is currently no active component instance to handle the injection of lifecycle hooks. The injection of lifecycle hooks can only be performed during setup
synchronous execution. If we want to inject lifecycle hooks in the async
form of asynchronous setup
, we must ensure that Before the first one await
.
From the source code to see the phenomenon
Dependency Gathering and Dispatching Updates in 2.0
When Vue 2.0 collects dependencies, it will store the currently created component instance Watcher
into this variable Dep.target
. This method can easily combine the current component instance and component needs. The variable dependencies are associated. The component needs to read some variables when it is created. These variables are encapsulated by defineReactive
, and there is a Dep
instance to maintain all the variables that depend on this variable Watcher
. When a variable is read, the getter
interceptor of this variable will register the currently-created component instance Dep.target
with its Dep
instance. . When the variable is updated, the variable's setter
interceptor will traverse the dep.subs
queue and notify each Watcher
to update update
. The author briefly described the process of dependency collection and distribution update in Vue 2.0. One of the key steps is to light the component currently being created to the global tag Dep.target
so that the dependency variable can collect it the dependent.
With this premise, the author boldly guesses that Vue 3.0 also uses a similar idea when dealing with the composition API of the life cycle, and lights the component instance currently being created on the global tag to complete the correct association between hooks and the component where it is located. Let's use the 3.0 source code to verify whether the author's conjecture is correct.
Definition of lifecycle hooks in 3.0
The author found the definition of life cycle hooks function in 3.0 packages/runtime-core/src/apiLifecycle.ts
.
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 const onServerPrefetch = createHook(LifecycleHooks.SERVER_PREFETCH)
export const onRenderTriggered = createHook<DebuggerHook>(
LifecycleHooks.RENDER_TRIGGERED
)
export const onRenderTracked = createHook<DebuggerHook>(
LifecycleHooks.RENDER_TRACKED
)
These hooks functions are new functions created by the createHook
function, and all pass in the values in the LifecycleHooks
enumeration to indicate their identity, let's take a look at createHook
what the function does.
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)
We look carefully and find that it directly returns a new function. When we call a hook like onMounted
in the business code, this function will be executed. It receives a hook
callback function, and An optional target
object, the default value is currentInstance
, we will analyze this target
object later. Let's ignore the situation of SSR first, the final execution injectHook
is the key operation, which literally means injecting the hook
callback function into the target
object's specified life cycle lifecycle
in.
We continued to dig injectHook
what did we do, the author cut out the unimportant parts.
export function injectHook(
type: LifecycleHooks,
hook: Function & { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false
): Function | undefined {
if (target) {
// 先根据指定的 lifecycle type 从 target 中寻找对应的 hooks 队列
const hooks = target[type] || (target[type] = [])
// 省略
// 这是经过包装的 hook 执行函数,用于在实际调用 hook 钩子时处理边界、错误等情况
const wrappedHook = /* 省略 */ (...args: unknown[]) => {
// 省略
const res = callWithAsyncErrorHandling(hook, target, type, args)
// 省略
return res
}
// 下面是关键步骤,我们发现它将 hook 回调函数注入到了对应生命周期的 hooks 队列中
if (prepend) {
hooks.unshift(wrappedHook)
} else {
hooks.push(wrappedHook)
}
return wrappedHook
} else if (__DEV__) {
// 省略
}
}
We can see that injectHook
is indeed injecting the hook callback hook into the corresponding lifecycle queue of target
. This is what happens after the onMounted
call.
target
what is
We have left a question above, that is, what is target
, we can easily associate it with its type description ComponentInternalInstance
and the default value currentInstance
An instance of the component is currently being created. We continue to verify our conjecture.
The author found its series of setting functions through the introduction location of currentInstance
packages/runtime-core/src/component.ts
.
export let currentInstance: ComponentInternalInstance | null = null
export const getCurrentInstance: () => ComponentInternalInstance | null = () =>
currentInstance || currentRenderingInstance
export const setCurrentInstance = (instance: ComponentInternalInstance) => {
currentInstance = instance
instance.scope.on()
}
export const unsetCurrentInstance = () => {
currentInstance && currentInstance.scope.off()
currentInstance = null
}
We continue to look for the call source of setCurrentInstance
, the author found in the same file setupStatefulComponent
function called it,
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
// 省略
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 省略
// 2. call setup()
const { setup } = Component
if (setup) {
// 创建 setup 函数上下文入参
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 关键步骤,点亮当前组件实例,必须在 setup 函数被调用前
setCurrentInstance(instance)
pauseTracking()
// 调用 setup 函数
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
// 关键步骤,解除当前点亮组件的设置
unsetCurrentInstance()
// 省略
} else {
// 省略
}
}
Similarly, we can see that in the key logic, the operation of lighting the instance of the component that is currently being created before the function call setup
is still not clear. Explain that this instance
( target
) is an instance of a component. We continue to look up and find a function called mountComponent
, in which the creation of the component instance and the call of setupComponent
are performed.
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 省略
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
// 省略
setupComponent(instance)
// 省略
}
// 省略
}
So far, we know that target
is indeed the component instance currently being created, and we have learned the entire process of association between lifecycle hooks and the component instance where they are located, and the conjecture has been verified.
more thinking
Clarify why lifecycle hooks cannot be invoked in async phases
Because Vue is non-blocking when calling the setup
function, which means that after the setup
function synchronous execution cycle ends, Vue immediately cancels the setting of the current lighting component, which is very It's easy to see why Vue warns about the injection of async lifecycle hooks.
setCurrentInstance(instance)
// 省略
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// 省略
unsetCurrentInstance()
How to inject lifecycle in async phase
According to Vue's suggestion, it is recommended that we inject the life cycle in the setup
synchronous execution cycle. In addition, by observing the input parameters of the function created by the createHook
function ( hook: T, target: ComponentInternalInstance | null = currentInstance
), we found that it allows us to manually pass a component instance for life cycle injection.
With this feature, combined with the official Vue getCurrentInstance
function is used to obtain the current component instance, the author boldly guesses, we can asynchronously inject the life cycle of the component destruction phase, let's try next one time.
<!-- parent.vue -->
<template>
<div>
<async-lifecycle v-if="isShow" />
<button @click="hide">hide</button>
</div>
</template>
<script lang="ts">
import { ref } from 'vue';
import AsyncLifecycle from './async-lifecycle.vue';
export default {
components: {AsyncLifecycle},
setup() {
const isShow = ref(true);
const hide = () => {
isShow.value = false;
};
return {
isShow,
hide,
};
},
};
</script>
We first define a parent component, and introduce a child component in it async-lifecycle
, which is controlled by the parent component to show or hide, and it is displayed by default. Subcomponents are defined as follows:
<!-- async-lifecycle.vue -->
<template>
<div />
</template>
<script lang="ts">
import { getCurrentInstance, onUnmounted } from 'vue';
export default {
setup() {
// getCurrentInstance 函数也必须在 setup 同步周期内调用
const instance = getCurrentInstance();
setTimeout(() => {
// 异步注入组件卸载时的生命周期
onUnmounted(() => {
console.log('unmounted');
}, instance);
});
},
}
</script>
When we clicked the button of the parent component to hide the child component, we found that the console output unmounted
, the guess is successful!
summary
We learned how the composition API of the lifecycle class is associated with component instances through the Vue source code. Some other types of APIs related to the current component are also similar. Interested students can learn about their implementation principles. With this knowledge, we should be careful when writing the Composition API and avoid asynchronous calls as much as possible. If we find similar problems during debugging, we can follow this line of thinking to find inappropriate call timing.
Finally, I recommend a shortcut mini-vue to learn the implementation principle of Vue 3.0.
When we need to learn vue3 in depth, we need to look at the source code to learn, but like this industrial-level library, there is a lot of logic in the source code to deal with edge cases or compatible processing logic, which is not conducive to our learning.
We should focus on the core logic, and the purpose of this library is to strip out the core logic in the vue3 source code for everyone to learn.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。