3
头图

公众号名片
作者名片

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.

For more exciting things, please pay attention to our public account "Hundred Bottles Technology", there are irregular benefits!


百瓶技术
127 声望18 粉丝

「百瓶」App 技术团队官方账号。