Vue3源码解析系列第一篇,笔者带领大家一起走过实例化一个Vue对象的过程,看@vue/compiler-core编译一起模块,代码生成器首先出现-生成模块。为了帮助您复习,让我们看一下编译过程中发生了什么。导出函数baseCompile(模板:字符串|RootNode,选项:CompilerOptions={}):CodegenResult{constonError=options.onError||defaultOnErrorconstisModuleMode=options.mode==='module'constprefixIdentifiers=!__BROWSER__&&(options.prefixIdentifiers===true||isModuleMode)//生成AST抽象语法树constast=isString(template)?baseParse(template,options):templateconst[nodeTransforms,directiveTransforms]=getBaseTransformPreset(prefixIdentifiers)//AST抽象语法树执行转换transform(ast,extend({},options,{}))//返回生成的代码字符串代码生成器returngenerate(ast,extend({},options,{prefixIdentifiers}))}在编译模块的简化源代码中,可以看到我们提到的AST抽象语法树的生成之前的文章,还有节点转换器的转换节点的评论。今天我们要说的是最后一行代码中的generate函数。发生了一些事。什么是代码生成器什么是代码生成器?它有什么作用?在回答这些问题之前,我们还是要从编译过程说起。在生成Vue对象的编译过程结束时,我们会从编译结果中得到一个名为code的字符串类型的变量。而这个变量就是我们今天整篇文章都会提到的代码串。Vue会使用这个生成的代码串,配合Function类的构造函数生成render渲染函数,最终使用生成的渲染函数完成相应组件的渲染。源码实现如下。functioncompileToFunction(template:string|HTMLElement,options?:CompilerOptions):RenderFunction{constkey=template//执行编译函数并根据结果构造代码字符串const{code}=compile(template,extend({hoistStatic:true,onError:__DEV__?onError:undefined,onWarn:__DEV__?e=>onError(e,true):NOOP}asCompilerOptions,options))//通过Function构造方法,生成render函数constrender=(__GLOBAL__?newFunction(code)():newFunction('Vue',code)(runtimeDom))asRenderFunction;(renderasInternalRenderFunction)._rc=true//返回生成的渲染函数和缓存return(compileCache[key]=render)那么接下来,笔者将带大家直接进入代码生成器模块,从generate函数开始,看看生成器是如何工作的。代码生成上下文generate函数位于packages/compiler-core/src/codegen.ts,我们先来看看它的函数签名。exportfunctiongenerate(ast:RootNode,options:CodegenOptions&{onContextCreated?:(context:CodegenContext)=>void}={}):CodegenResult{constcontext=createCodegenContext(ast,options)/*忽略后续逻辑*/}exportinterfaceCodegenResult{code:stringpreamble:stringast:RootNodemap?:RawSourceMap}generate函数,接收两个参数,分别是转换器处理后的ast抽象语法树,以及options代码生成选项。最后,返回一个CodegenResult类型的对象。可以看出,CodegenResult包含代码代码串,ast抽象语法树,可选的sourceMap,以及代码串的序言。至于generate函数,第一行是生成一个context对象。这里,为了更好地理解语义,我们将这个上下文称为代码生成器的上下文对象,或者简称为生成器上下文。除了生成器上下文中的一些属性之外,您会注意到它还有5个实用函数。这里我们主要关注推送功能。在讲解push之前,没看过代码的朋友可能会有点懵。将元素添加到数组的函数有什么可说的?但是这个推送不是另一个推送,我先给大家展示一下推送的实现。push(code,node){context.code+=codeif(!__BROWSER__&&context.map){if(node){letname/*忽略逻辑*/addMapping(node.loc.start,name)}advancePositionWithMutation(context,code)if(node&&node.loc!==locStub){addMapping(node.loc.end)}}}看了上面push的实现,我们可以发现push并不是将元素压入数组,但要连接字符串,请将传入的字符串连接到上下文中的代码属性中。并且会调用addMapping生成对应的sourceMap。这个功能非常重要。当生成器处理完ast树中的每个节点后,会调用push将新生成的字符串拼接到之前生成的代码字符串中。直到最后,得到完整的代码串,作为结果返回。除了上下文中的push之外,还有indent、deindent、newline函数用于处理字符串位置。各自的功能是缩进、向后缩进和插入新行。用于辅助生成代码串和格式化结构,使得生成的代码串非常直观,就像在ide中打字一样。并且上下文中有一个执行过程。创建生成器上下文后,会向下执行生成函数。接下来笔者将继续和大家一起阅读分析生成器的执行过程。我在本节中放置的所有代码都在generate函数体中,因此为了简洁起见,将不再重复generate的函数签名。代码串前缀内容生成consthelpers=ast.helpers.length>0//是否有helpers辅助函数constuseWithBlock=!prefixIdentifiers&&mode!=='module'//使用with扩展作用域constgenScopeId=!__BROWSER__&&scopeId!=null&&mode==='module'//不在浏览器环境下mode为moduleif(!__BROWSER__&&mode==='module'){//使用ES模块标准导入导入helper辅助函数,处理生成代码的前导部分genModulePreamble(ast,preambleContext,genScopeId,isSetupInlined)}else{//否则生成代码的前导部分是单个const{helpers...}=Vue处理codegenFunctionPreamble(ast,preambleContext)}创建上下文后,从上下文中解构一些对象,会生成代码字符串的序言。这里的关键判断是mode属性。根据mode属性判断使用哪种方式引入helpers。函数的声明。模式有两个选项,“模块”或“功能”。当传入参数为module时,ast中的helpers辅助函数会通过ES模块的import导入,render函数默认会用export导出。当传入的参数是一个函数时,会生成一个单独的const{helpers...}=Vue声明,return返回渲染函数而不是导出它。在下面的代码框里,我把两种模式生成的前置码的区别写了出来。//mode==='module'generatedpre-part'import{createVNodeas_createVNode,resolveDirectiveas_resolveDirective}from"vue"export'//mode==='function'generatedpre-part'const{createVNode:_createVNode,resolveDirective:_resolveDirective}=Vuereturn'需要注意的是,上面的代码只是前面部分代码。我们还没有开始解析其他资源和节点,所以当我们到达出口或返回时它突然停止。了解了pre-parts之间的区别之后,让我们继续看代码。生成渲染函数签名接下来,生成器将开始生成渲染函数的函数体,从函数名和传递给渲染函数的参数开始。函数签名确定后,如果mode为function,生成器会使用with来扩大作用域,最终生成的样子在第一次编译过程中也已经展示过了。首先会根据是否服务端渲染判断函数名functionName和传入函数的参数args,函数签名部分ssr标记判断是否为TypeScript环境。如果是TypeScript,参数将被标记为任何类型。然后会判断是通过箭头函数还是函数声明来创建函数。函数创建后,函数体会判断是否需要通过with扩展作用域,此时如果有helpers辅助函数,也会在with的块级作用域中进行析构,变量解构后会重命名,防止与用户变量名冲突。具体代码逻辑如下。//生成的函数名constfunctionName=ssr?`ssrRender`:`render`//函数参数传递constargs=ssr?['_ctx','_push','_parent','_attrs']:['_ctx','_cache']/*忽略逻辑*///函数签名,如果是TypeScript,则标记为任意类型constsignature=!__BROWSER__&&options.isTS?args.map(arg=>`${arg}:any`).join(','):args.join(',')/*忽略逻辑*///使用箭头函数或函数声明创建渲染函数if(isSetupInlined||genScopeId){push(`(${signature})=>{`)}else{push(`function${functionName}(${signature}){`)}indent()//使用扩展范围if(useWithBlock){push(`with(_ctx){`)indent()//在函数模式下,const声明应该在代码块中,//解构变量应该重命名以防止变量名称与用户变量名称冲突if(hasHelpers){push(`const{${ast.helpers.map(s=>`${helperNameMap[s]}:_${helperNameMap[s]}`).join(',')}}=_Vue`)push(`\n`)newline()}}资源分解声明在看到小标题“资源分解声明”之前,我们需要了解一下生成器将什么定义为资源。generator将解析出的AST抽象语法树组件、directives指令、temps临时变量、以及上个月在Vue3中兼容Vue2过滤器的四类过滤器作为资源。在render函数中,这部分的处理会预先声明以上所有的资源,将从AST树解析出来的资源id传递给每个资源对应的处理函数,并生成相应的资源变量。//如果ast中有组件,解析组件if(ast.components.length){genAssets(ast.components,'component',context)if(ast.directives.length||ast.temps>0){newline()}}/*省略指令和过滤器,逻辑与组件一致*/if(ast.temps>0){push(`let`)for(leti=0;i
