当前位置: 首页 > Web前端 > vue.js

Vue3源码解析(三):静态提升

时间:2023-03-31 16:39:12 vue.js

什么是静态提升在Vue3正式版发布之前,游达在一篇关于Vue3的分享中提到了静态提升。在阅读源码时,静态改进也是笔者的一个重点阅读点。那么什么是静态提升?当Vue编译器发现一些在编译过程中不会改变的节点或属性时,它会标记这些节点。然后在生成代码串的过程中,编译器会发现这些静态节点,升级它们,序列化成字符串,从而降低编译和渲染成本。有时可以跳过整棵树。

Static{{dynamic}}
比如这段模板代码,不用怀疑,我们可以看到这个节点,无论动态表达式如何变化,它都不会再变化了。对于这样的节点,可以将它们标记为静态提升。而Vue3也可以静态提升props属性。{{text}}
比如这段模板代码,Vue3会跳过节点,只会不再改变id="foo"和class="酒吧”进行宣传。编译代码字符串在上面的例子中,我们简单的分析了一些模板。下面我们通过一个例子来了解一下静态提升前后的变化。
看这样一个模板,符合静态提升的条件,但是如果没有静态提升机制,就会编译如下代码:const{createVNode:_createVNode,openBlock:_openBlock,createBlock:_createBlock}=Vuereturnfunctionrender(_ctx,_cache){return(_openBlock(),_createBlock("div",null,[_createVNode("div",null,[_createVNode("span",{class:"foo"}),_createVNode("span",{class:"foo"}),_createVNode("span",{class:"foo"}),_createVNode("span",{class:"foo"}),_createVNode("span",{class:"foo"})])]))}编译后生成的render函数很清晰,是柯里化函数,并且返回一个函数,创建一个根节点的div,在children中创建一个div元素,最后在最里面的div节点创建五个span子元素。如果是静态提升,会这样编译:const{createVNode:_createVNode,createStaticVNode:_createStaticVNode,openBlock:_openBlock,createBlock:_createBlock}=Vueconst_hoisted_1=/*#__PURE__*/_createStaticVNode("
",1)返回函数render(_ctx,_cache){return(_openBlock(),_createBlock("div",null,[_hoisted_1]))}静态提升后生成的代码,我们可以看到有明显的区别,会生成一个变量:_hoisted_1,并标记为/*#__PURE__*/。_hoisted_1调用createStaticVNode通过传递一个字符串参数来创建一个静态节点。而_createBlock中,原本传入多个创建节点的函数,只传入一个函数,性能上的提升不言而喻。了解了静态提升的现象,我们再来看看源码中的实现。上篇文章中提到的transform转换器,编译时会调用compiler-core模块中@vue/compiler-core/src/compile.ts文件下的baseCompile函数。在这个函数执行过程中,会执行transform函数,传入解析后的AST抽象语法树。那么我们先看看transform函数做了什么。exportfunctiontransform(root:RootNode,options:TransformOptions){//创建一个转换上下文constcontext=createTransformContext(root,options)//遍历所有节点并进行转换traverseNode(root,context)//如果在compilationoptionsIf(options.hoistStatic){hoistStatic(root,context)}if(!options.ssr){createRootCodegen(root,context)}//确定最终元信息root.helpers=[...context.helpers.keys()]root.components=[...context.components]root.directives=[...context.directives]root.imports=context.importsroot.hoists=context.hoistsroot.temps=context.tempsroot.cached=context.cached}transform函数很短,从中文注释可以注意到第7行代码的位置,转换器在编译时判断是否有启用静态提升的开关。如果开启的话,节点会被静态提升。今天笔者的文章主要介绍静态提升,下面就来探究一下静态提升的代码,其余代码就不展开详细研究了。hoistStatichoistStatic函数源码如下:exportfunctionhoistStatic(root:RootNode,context:TransformContext){walk(root,context,//遗憾的是根节点不能静态提升isSingleElementRoot(root,root.children[0]))}从函数的声明中我们可以知道,静态提升转换器接收根节点和转换器上下文作为参数。只需调用walk函数即可。walk函数很长,在讲解walk函数之前,我先把walk函数的函数签名写出来给大家说说。(node:ParentNode,context:TransformContext,doNotHoistNode:boolean)=>void从函数签名可以看出,walk函数的参数需要一个node节点,context转换器的上下文,doNotHoistNode等布尔值从外部通知该节点是否可以被提升。hoistStatic函数中传入了根节点,不能吊起根节点。walk函数接下来笔者将分段为大家解析walk函数。functionwalk(node:ParentNode,context:TransformContext,doNotHoistNode:boolean=false){让hasHoistedNode=false让canStringify=trueconst{children}=nodefor(leti=0;iConstantTypes.NOT_CONSTANT){//根据constantType枚举值判断是否可以按字符序列化if(constantType=ConstantTypes.CAN_HOIST){//然后将子节点的codegenNode属性的patchFlag标记为HOISTED进行提升;(child.codegenNodeasVNodeCall).patchFlag=PatchFlags.HOISTED+(__DEV__?`/*HOISTED*/`:``)child.codegenNode=context.hoist(child.codegenNode!)//hasHoistedNode被记录为truehasHoistedNode=truecontinue}}else{//节点可能包含动态子节点,但它的props属性也可能被合法提升constcodegenNode=child.codegenNode!if(codegenNode.type===NodeTypes.VNODE_CALL){//获取patchFlagconstflag=getPatchFlag(codegenNode)//如果没有flag,或者flag是文本类型//而节点props的constantType值可以被提升if((!flag||flag===PatchFlags.NEED_PATCH||flag===PatchFlags.TEXT)&&getGeneratedPropsConstantType(child,context)>=ConstantTypes.CAN_HOIST){//获取节点的props并执行转换器上下文中的提升操作constprops=getNodeProps(child)if(props){codegenNode.props=context.hoist(props)}}}}//如果节点类型是TEXT_CALL,检查相同,逻辑是和之前一样}elseif(child.type===NodeTypes.TEXT_CALL){constcontentType=getConstantType(child.content,context)if(contentType>0){if(contentType=ConstantTypes.CAN_HOIST){child.codegenNode=context.hoist(child.codegenNode)hasHoistedNode=true}}}//walkfurther/*暂时忽略*/}循环体中的函数比较长,下面walkfurther的部分我们先不关注。为了便于理解,我逐行添加注释。通过最外层if分支顶部的注释,我们可以知道只能提升简单元素和文本类型,所以会先判断该节点是否为元素类型。如果节点是元素,则检查walk函数的doNotHoistNode参数以查看是否可以提升节点。如果doNotHoistNode不为真,则调用getConstantType函数获取当前节点的constantType。exportconstenumConstantTypes{NOT_CONSTANT=0,CAN_SKIP_PATCH,CAN_HOIST,CAN_STRINGIFY}这是ConstantType枚举的声明,通过它可以将静态类型分为4个层次,静态类型较高层次的节点覆盖较小的值节点都是能力。例如,当一个节点被标记为CAN_STRINGIFY时,意味着它可以被字符序列化,因此它将始终是一个可以静态提升的节点(CAN_HOIST)并跳过PATCH检查。了解了ConstantType类型之后,再看后续的判断。获取到元素类型节点的静态类型后,会判断静态类型的值是否大于NOT_CONSTANT。如果条件为真,则意味着该节点可能被提升或字符序列化。然后再往下判断静态类型是否可以按字符序列化,如果不能则修改canStringify标记。然后判断静态类型是否可以吊装。如果可以提升,将子节点的codegenNode对象的patchFlag属性标记为PatchFlags.HOISTED,在convertercontext中执行context.hoist操作,修改hasHoistedNode的flag。至此元素类型节点的晋升判断就完成了,我们发现有一个PatchFlags标志位。你只需要知道PatchFlags就是在编译过程中产生的一些优化标记。后续代码是判断节点不是简单元素时,尝试改进节点props中的static属性,当节点是文本类型时,确认是否需要改进。限于篇幅,请自行查看以上代码。我在前面隐藏了一段walkfurther的逻辑。从评论中了解到,这段代码的作用是继续检查一些分支,看是否有静态改进的可能。代码如下://走得更远if(child.type===NodeTypes.ELEMENT){//如果子节点的tagType是组件,则继续遍历子节点//判断中的情况slotconstisComponent=child.tagType===ElementTypes.COMPONENTif(isComponent){context.scopes.vSlot++}walk(child,context)if(isComponent){context.scopes.vSlot--}}elseif(child.type===NodeTypes.FOR){//检查类型为v-for的节点是否可以提升//但是如果v-for节点中只有一个子节点,则不能提升walk(child,context,child.children.length===1)}elseif(child.type===NodeTypes.IF){//如果子节点是v-if类型,则判断其所有分支条件为(leti=0;i