前言我们在使用Vue3.0CompositionAPI时,通常会在setupcycle中使用生命周期钩子函数(onMounted、onBeforeDestroy等)来完成注入生命周期钩子。调用这些API有什么限制吗?答案是肯定的。让我们通过一个例子来看看这个现象。在不同阶段注入生命周期钩子vs.同步阶段这就是我们通常编写生命周期注入的方式。当组件加载完成后,控制台可以正常打印出mounted。异步阶段如果我们突然想在异步阶段注入生命周期会怎样?exportdefault{setup(){setTimeout(()=>{onMounted(()=>{console.log('mounted');});});},};此时我们会发现控制台输出一个Vue警告:[Vuewarn]:onMountediscalledwhenthereisnoactivecomponentinstancetobeassociated.生命周期注入API只能在执行setup()期间使用。如果您使用的是异步设置(),请确保在第一个await语句之前注册生命周期挂钩。大概意思是当onMounted被调用时,当前没有活跃的组件实例来处理生命周期钩子的注入。生命周期挂钩的注入只能在安装程序的同步执行期间完成。如果我们想以async的形式将生命周期钩子注入到异步设置中,我们必须确保它在第一次await之前完成。从源码看Phenomena2.0中的依赖收集和分发UpdateVue2.0在进行依赖收集时会将当前创建的组件实例Watcher存储到变量Dep.target中。该方法可以方便地将当前组件实例与组件所需的变量依赖关联起来。组件在创建时需要读取一些变量。这些变量被defineReactive封装起来,有一个Dep实例来维护所有依赖这个变量的Watcher。当一个变量被读取时,这个变量的getter拦截器会将当前正在创建的组件实例Dep.target注册到它的Dep实例上。当变量更新时,变量的setter拦截器会遍历dep.subs队列,通知每个Watcher更新。笔者简单描述了Vue2.0中依赖收集和分布更新的过程。关键步骤之一是将当前正在创建的组件点亮到全局标记Dep.target,以便因变量可以收集其依赖项。以此为前提,笔者大胆猜测Vue3.0在处理生命周期CompositionAPI时也采用了类似的思路,将当前正在创建的组件实例高亮到全局标记,完成hooks与其所在组件的正确关联。下面我们就用3.0的源码来验证一下作者的猜想是否正确。3.0中生命周期钩子的定义笔者在3.0的packages/runtime-core/src/apiLifecycle.ts中找到了生命周期钩子函数的定义。导出常量onBeforeMount=createHook(LifecycleHooks.BEFORE_MOUNT)exportconstonUnmounted=createHook(LifecycleHooks.UNMOUNTED)exportconstonServerPrefetch=createHook(LifecycleHooks.SERVER_PREFETCH)exportconstonRenderTriggered=createHook(LifecycleHooks.RENDER_TRIGGERED)exportconstonRenderTracked=createHook(LifecycleHooks.RENDER_TRIGGERED)exportconstonRenderTracked(LifecycleHooks.RENDER_TRACKES)hook被跟踪createHook函数创建的新函数都是传入LifecycleHooks枚举中的值来标识自己。我们来看看createHook函数是干什么的。exportconstcreateHook=any>(lifecycle:LifecycleHooks)=>(hook:T,target:ComponentInternalInstance|null=currentInstance)=>//post-createlifecycleregistrationsarenoopsduringSSR(exceptforserverPrefetch)(!isInSSRComponentSetup||lifecycle===LifecycleHooks.SERVER_PREFETCH)&&injectHook(lifecycle,hook,target)我们仔细看了一下,发现直接返回了一个新的函数。当我们在业务代码中调用onMounted这样的hook时,它会执行这个函数,它接收到一个hook回调函数,以及一个可选的目标对象,默认值为currentInstance,这个目标对象是什么我们后面会分析。先不考虑SSR的情况,最后执行injectHook才是关键操作,字面意思就是将hook回调函数注入到目标对象的指定生命周期中。我们继续深挖injectHook是干什么的,作者删掉了不重要的部分。导出函数injectHook(类型:LifecycleHooks,钩子:函数&{__weh?:函数},目标:ComponentInternalInstance|null=currentInstance,前置:boolean=false):函数|undefined{if(target){//先根据指定的生命周期类型从target中找到对应的hooks队列consthooks=target[type]||(target[type]=[])//omitted//这是wrappedhook执行函数,用来处理hook真正调用Boundary时,error等constwrappedHook=/*omitted*/(...args:unknown[])=>{//省略constres=callWithAsyncErrorHandling(hook,target,type,args)//省略returnres}//下面是关键步骤,我们发现它把hook回调函数注入了生命周期对应的hooks队列if(prepend){hooks.unshift(wrappedHook)}else{hooks.push(wrappedHook)}returnwrappedHook}elseif(__DEV__){//省略}}我们可以看到injectHook确实在注入hook回调挂钩到target对应的生命周期队列中。这是调用onMounted后发生的情况。目标是什么?上面我们保留一个问题,就是目标是什么?我们很容易根据其类型描述ComponentInternalInstance和默认值currentInstance,认为它是当前正在创建的组件的实例。我们继续验证我们的猜想。笔者通过导入位置packages/runtime-core/src/component.ts找到了currentInstance的一系列设置函数。导出让当前实例:ComponentInternalInstance|null=nullexportconstgetCurrentInstance:()=>ComponentInternalInstance|null=()=>当前实例||currentRenderingInstanceexportconstsetCurrentInstance=(instance:ComponentInternalInstance)=>{currentInstance.instance)=}exportconstunsetCurrentInstance=()=>{currentInstance&¤tInstance.scope.off()currentInstance=null}让我们继续寻找setCurrentInstance调用的来源。作者发现setupStatefulComponent函数在同一个文件中调用了它。functionsetupStatefulComponent(instance:ComponentInternalInstance,isSSR:boolean){constComponent=instance.typeasComponentOptions//省略//0.创建渲染代理属性访问缓存instance.accessCache=Object.create(null)//1.创建公共实例/renderproxy//也将其标记为原始的,因此它永远不会被观察到//省略//2.callsetup()const{setup}=Componentif(setup){//创建设置函数上下文输入参数constsetupContext=(instance.setupContext=setup.length>1?createSetupContext(instance):null)//关键步骤,点亮当前组件实例,必须在调用setCurrentInstance(instance)pauseTracking()函数之前//调用设置函数constsetupResult=callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__?shallowReadonly(instance.props):instance.props,setupContext])resetTracking()//关键一步,取消当前点亮组件的设置unsetCurrentInstance()//省略}else{//省略}}也删除不重要的逻辑。我们可以看到在关键逻辑中,确实在setup函数调用之前对当前正在创建的组件实例进行了点亮操作,但是目前还不清楚这个实例(target)就是组件的实例.我们继续向上查找,找到了一个叫做mountComponent的函数,在这个函数中进行了组件实例的创建和setupComponent的调用。constmountComponent:MountComponentFn=(initialVNode,container,anchor,parentComponent,parentSuspense,isSVG,optimized)=>{//2.xcompat可能会在实际安装之前预先创建组件实例//constcompatMountInstance=__COMPAT__&&initialVNodeis.initialVNode.component常量实例:ComponentInternalInstance=compatMountInstance||(initialVNode.component=createComponentInstance(initialVNode,parentComponent,parentSuspense))//省略//为设置上下文解析道具和插槽if(!(__COMPAT__&&compatMount{//省略)setupComponent(instance)//省略}//省略}至此,我们知道目标确实是当前正在创建的组件实例,了解了生命周期钩子与其所在组件实例关联的全过程,猜想也得到了验证。更多的思考使得很明显在异步阶段不能调用生命周期钩子,因为Vue在调用setup函数时是非阻塞的,也就是说setup函数的同步执行周期结束后,Vue会立即unset掉当前点亮的组件。很简单了解为什么Vue会警告注入异步生命周期挂钩。setCurrentInstance(instance)//省略constsetupResult=callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__?shallowReadonly(instance.props):instance.props,setupContext])//省略unsetCurrentInstance()如何在异步中注入生命周期phase根据Vue的建议,建议我们在setup同步执行周期中注入生命周期。另外,通过观察createHook函数创建的函数的入参(hook:T,target:ComponentInternalInstance|null=currentInstance),我们发现它允许我们手动传递一个组件实例进行生命周期注入。利用这个特性,结合Vue官方提供的获取当前组件实例的getCurrentInstance函数,笔者大胆猜测,我们可以异步注入组件销毁阶段的生命周期。接下来我们试试看。我们先定义一个父组件,引入子组件async-lifecycle,它的显示由父组件控制,默认显示。子组件定义如下:当我们点击父组件的按钮隐藏子组件时,发现控制台输出unmounted,猜测成功!总结我们通过Vue源码了解了生命周期类的CompositionAPI与组件实例的关联。与当前组件关联的一些其他类型的API也有类似的想法。有兴趣的同学可以了解一下它们的实现原理。有了这些认知,我们在写CompositionAPI的时候就要小心了,尽量避免异步调用。如果调试时发现类似问题,我们可以按照这个思路寻找不合适的调用机会。最后推荐一个学习Vue3.0实现原理的捷径mini-vue。当我们需要深入学习vue3的时候,需要看源码来学习,但是像这种工业级的库,源码里面有很多处理edgecase或者兼容处理的逻辑,这不利于我们的学习。我们应该专注于核心逻辑,而这个库的目的是将核心逻辑从vue3源码中分离出来,供大家学习。更多精彩,敬请关注我们的公众号《百瓶科技》,不定期有福利哦!