Vue3源码解析(九):setup的秘密与暴露的魔法——setup。大多数情况下,我们编写的组件都是有状态组件,这样的组件在初始化时会被标记为有状态组件。当Vue3检测到我们正在处理这样的有状态组件时,它会调用函数setupStatefulComponent来初始化一个有状态组件。处理组件部分的源码位置为:@vue/runtime-core/src/component.ts。setupStatefulComponent接下来笔者就带大家分析一下setupStatefulComponent的过程:等,如果有错误就报警*/}//0.为渲染代理实例的属性创建一个访问缓存。accessCache=Object.create(null)//1.创建一个公共实例或渲染代理//它将被标记为原始的,因此不会被跟踪instance.proxy=newProxy(instance.ctx,PublicInstanceProxyHandlers)//2.调用setup()const{setup}=Componentif(setup){constsetupContext=(instance.setupContext=setup.length>1?createSetupContext(instance):null)currentInstance=instancepauseTracking()constsetupResult=callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__?shallowReadonly(instance.props):instance.props]续ext,)resetTracking()currentInstance=nullif(isPromise(setupResult)){if(isSSR){//返回一个promise,以便服务器端渲染可以等待它执行returnsetupResult.then((resolvedResult:unknown)=>{handleSetupResult(instance,resolvedResult,isSSR)}).catch(e=>{handleError(e,instance,ErrorCodes.SETUP_FUNCTION)})}}else{//捕获Setup执行结果handleSetupResult(instance,setupResult,isSSR)}}else{//完成组件初始化finishComponentSetup(instance,isSSR)}}组件会初始化一个Component变量一开始,它包含组件的选项。接下来,如果是DEV环境,会开始检测组件中各种选项的命名,比如name、components、directives等,如果检测有问题,会在develop中报警告环境。检测完成后,就会开始正式的初始化过程。首先,会在实例上创建一个accessCache属性,用于缓存渲染器代理属性以减少读取次数。然后在组件实例上初始化一个代理属性,它代理组件的上下文,并将其设置为观察原始值,这样代理对象就不会被跟踪。之后,我们将开始处理我们在本文中关心的设置逻辑。首先,从组件中取出设置函数。这里判断是否有setup函数。如果不存在,则直接跳转到底层逻辑,执行finishComponentSetup,完成组件初始化。否则进入if(setup)后的分支条件。是否执行createSetupContext生成setupcontext对象取决于setup函数中的形参个数是否大于1。这里需要注意的一个知识点是:在函数函数对象上调用length时,返回值是这个函数的形参量。例如:setup()//setup.length===0setup(props)//setup.length===1setup(props,{emit,attrs})//setup.length===2默认情况下,props是调用setup时必须传递的参数,所以生成setup上下文的条件是setup.length>1。那么按照代码逻辑,我们来看看setup上下文中有什么。exportfunctioncreateSetupContext(instance:ComponentInternalInstance):SetupContext{constexpose:SetupContext['expose']=exposed=>{instance.exposed=proxyRefs(exposed)}if(__DEV__){/*DEV逻辑忽略,为上下文选项设置getter*/}else{return{attrs:instance.attrs,slots:instance.slots,emit:instance.emit,expose}}}expose的妙用看到这个createSetupContext函数的逻辑,我们发现设置上下文只是就像文档中描述的那样,有三个熟悉的属性:attrs、slots、emit,但是在这里我惊奇的发现有一个文档中没有描述的expose属性要返回。Expose是早期VueRFC中的一个提案。expose的思想是提供一个像expose({...publicMembers})这样的复合API,让组件的作者可以在setup()中使用这个API来明确控制哪些内容会被显式公开给组件消费者。大家在封装组件的时候,如果觉得ref里面暴露的内容太多,还不如用expose来限制输出。当然,这只是一个RFC提案,有兴趣的朋友可以偷偷试用一下。import{ref}from'vue'exportdefault{setup(_,{expose}){constcount=ref(0)functionincrement(){count.value++}//只将increment暴露给父组件expose({increment})return{increment,count}}}比如像上面代码这样使用expose时,父组件获取到的ref对象只会有increment属性,count属性不会暴露。执行setup函数在处理完setupContext上下文后,组件将停止依赖收集并开始执行setup函数。constsetupResult=callWithErrorHandling(setup,instance,ErrorCodes.SETUP_FUNCTION,[__DEV__?shallowReadonly(instance.props):instance.props,setupContext])Vue会通过callWithErrorHandling调用setup函数,这里我们可以看到最后一行,是passed作为args参数如上所述,props将始终被传入。如果setup.length<=1,setupContext将为null。调用setup后,依赖集合状态被重置。接下来判断setupResult的返回值类型。如果setup函数的返回值是promise类型,并且是服务端渲染的,会等待继续执行。否则会报错,说当前版本的Vue不支持setup返回promise对象。如果返回值不是promise类型,返回结果会通过handleSetupResult函数处理。exportfunctionhandleSetupResult(instance:ComponentInternalInstance,setupResult:unknown,isSSR:boolean){if(isFunction(setupResult)){//安装程序返回一个内联渲染函数if(__NODE_JS__&&(instance.typeasComponentOptions).__ssrInlineRender){//当该函数的名称为ssrRender时(SFC内联方式编译)//将函数作为服务端渲染函数instance.ssrRender=setupResult}else{//否则将函数作为渲染函数instance.render=setupResultasInternalRenderFunction}}elseif(isObject(setupResult)){//将返回的对象转换为响应对象,并在结果捕获中将其设置为实例的setupState属性instance.setupState=proxyRefs(setupResult)}finishComponentSetup(instance,isSSR)}functionhandleSetupResult,首先判断setup返回结果的类型,如果是函数,并且也是服务端内联模式渲染函数,则将结果作为thessrRender属性;在非服务器渲染的情况下,它会被直接视为渲染函数。然后会判断如果setup返回的结果是一个对象,就会把这个对象转换成一个代理对象,并设置为组件实例的setupState属性。最后和其他没有setup函数的组件一样,调用finishComponentSetup完成组件的创建。finishComponentSetup函数的主要作用是获取并设置组件的渲染函数。模板(template)和渲染函数的获取方式有三种标准行为:1.渲染函数可能已经存在,通过setup返回结果。比如我们上一节提到setup的返回值是一个函数。2.如果setup没有返回,尝试获取组件模板并编译,从Component.render获取渲染函数。3.如果该函数仍然没有渲染函数,将instance.render设置为空,这样就可以从mixins/extend等其他方式获取渲染函数。在这种规范行为的指导下,它先判断服务端渲染的情况,再判断没有instance.render的情况。在做这个判断的时候,已经说明组件没有从setup中获取渲染函数。尝试两种行为。从组件中获取模板并在设置编译选项后调用Component.render=compile(template,finalCompilerOptions)进行编译。编译这部分的知识在我的第一篇文章编译过程中有详细介绍。最后将编译好的渲染函数赋值给组件实例的render属性,如果没有则赋值给一个NOOP空函数。然后判断渲染函数是否是with块包裹的运行时编译渲染函数。如果是这种情况,渲染代理会被设置为不同的hashandlerproxytrap,性能更强,可以避免检测到一些全局变量。至此,组件的初始化就完成了,渲染功能也设置好了。exportfunctionfinishComponentSetup(instance:ComponentInternalInstance,isSSR:boolean,skipOptions?:boolean){constComponent=instance.typeasComponentOptions//渲染函数的模板/标准行为//1.渲染函数可能已经存在,通过setup返回//2.另外,尽量使用`Component.render`作为渲染函数//3.如果这个函数没有渲染函数,将`instance.render`设置为空函数,这样就可以获取渲染函数frommixins/extendif(__NODE_JS__&&isSSR){instance.render=(instance.render||Component.render||NOOP)asInternalRenderFunction}elseif(!instance.render){//可以在setup()中设置if(compile&&!Component.render){consttemplate=Component.templateif(template){const{isCustomElement,compilerOptions}=instance.appContext.configconst{delimiters,compilerOptions:componentCompilerOptions}=ComponentconstfinalCompilerOptions:CompilerOptions({Ctomextment=扩展d(结束limiters},compilerOptions),componentCompilerOptions)Component.render=compile(template,finalCompilerOptions)}}instance.render=(Component.render||NOOP)asInternalRenderFunction//对于使用`with`块的运行时编译渲染函数,这个渲染代理需要一个不同的`has`处理程序陷阱,它具有更好的//性能并且只允许白名单全局属性通过if(instance.render._rc){instance.withProxy=newProxy(instance.ctx,RuntimeCompiledPublicInstanceProxyHandlers)}}}小结今天笔者介绍了一个有状态组件的初始化过程,并在setup函数初始化部分进行了仔细的讲解。我们不仅了解了setupcontext的初始化条件,还清楚地知道了setupcontext有哪些属性暴露给我们,并且从中学到了一个新的RFC提案:exposeattribute。我们了解了setup函数如何执行以及Vue如何处理捕获setup的返回结果。最后我们解释了finishComponentSetup这个函数,这个函数在组件初始化的时候不管是否使用setup都会被执行。通过这个函数内部的逻辑,我们了解了组件初始化时设置渲染函数的规则。最后,如果这篇文章能帮助你了解Vue3中setup的小细节,希望你能给这篇文章点个赞??。如果想继续关注后续文章,也可以关注我的账号或者关注我的github。再次感谢所有可爱的读者。
