Vue3正式发布有一段时间了,前段时间写了一篇文章(《Vue 模板编译原理》)分析Vue的模板编译原理。今天这篇文章打算学习一下Vue3和Vue2下模板编译的区别,以及VDOM下Diff算法的优化。在编译入口了解过Vue3的同学一定知道,Vue3引入了一个新的组合API。组件挂载阶段会调用setup方法,然后判断render方法是否存在。如果没有,将调用编译方法将模板转换为渲染。//packages/runtime-core/src/renderer.tsconstmountComponent=(initialVNode,container)=>{constinstance=(initialVNode.component=createComponentInstance(//...params))//调用setupsetupComponent(instance)}//packages/runtime-core/src/component.tsletcompileexportfunctionregisterRuntimeCompiler(_compile){compile=_compile}exportfunctionsetupComponent(instance){constComponent=instance.typeconst{setup}=Componentif(setup){//...调用setup}if(compile&&Component.template&&!Component.render){//如果没有render方法//调用compile将template转换为render方法Component.render=compile(Component.template,{...})}}这部分是运行时的代码-core,上一篇文章提到Vue分为完整版和运行时版。如果使用vue-loader处理.vue文件,一般会直接将.vue文件中的模板处理到render方法中。//需要编译器Vue.createApp({template:'
{{hi}}}
'})//不需要Vue.createApp({render(){returnVue.h('div',{},this.hi)}})完整版和runtime版本的区别在于完整版会引入compile方法。如果是vue-cli生成的工程,这部分代码会被抹去,编译过程会放在打包阶段,优化性能。runtime-dom中提供了registerRuntimeCompiler方法用于注入compile方法。主要流程在完整版index.js中,调用registerRuntimeCompiler注入compile。接下来我们看看注入的compile方法主要做了什么。//packages/vue/src/index.tsimport{compile}from'@vue/compiler-dom'//编译缓存constcompileCache=Object.create(null)//注入编译方法functioncompileToFunction(//templatetemplate:string|HTMLElement,//编译配置选项?:CompilerOptions):RenderFunction{if(!isString(template)){//如果template不是字符串//认为是DOM节点,获取innerHTMLif(template.nodeType){template=template.innerHTML}else{returnNOOP}}//如果缓存中存在,直接从缓存中获取constkey=templateconstcached=compileCache[key]if(cached){returncached}//如果是ID选择器,获取DOM后element,取innerHTMLif(template[0]==='#'){constel=document.querySelector(template)template=el?el.innerHTML:''}//调用compile得到rendercodeconst{code}=compile(template,options)//Convertrendercodetofunctionconstrender=newFunction(code)();//同时返回render方法,放入缓存return(compileCache[key]=render)}//调用时注入compileregisterRuntimeCompiler(compileToFunction)关于Vue2模板编译前面已经讲过,编译方法主要分为三步,Vue3的逻辑类似:模板编译,将模板代码转成AST;优化AST,方便后续虚拟DOM更新;生成代码,并将AST转换为可执行代码;//packages/compiler-dom/src/index.tsimport{baseCompile,baseParse}from'@vue/compiler-core'exportfunctioncompile(template,options){returnbaseCompile(template,options)}//packages/compiler-core/src/compile.tsimport{baseParse}from'./parse'import{transform}from'./transform'import{transformIf}from'./transforms/vIf'import{transformFor}from'./transforms/vFor'import{transformText}from'./transforms/transformText'import{transformElement}from'./transforms/transformElement'import{transformOn}from'./transforms/vOn'import{transformBind}from'./transforms/vBind'import{transformModel}from'./transforms/vModel'exportfunctionbaseCompile(模板,选项){//解析html并转换为astconstast=baseParse(template,options)//优化ast,标记静态节点transform(ast,{...options,nodeTransforms:[transformIf,transformFor,transformText,transformElement,//...OmitTransform],directiveTransforms:{on:transformOn,bind:transformBind,model:transformModel}})//将ast转换成可执行代码returngenerate(ast,options)}计算PatchFlag的通用逻辑这里和上一个没有太大区别,不同的是optimize方法变成了transform方法,默认情况下,一些模板语法将被转换。这些转换是后续虚拟DOM优化的关键。我们先看一下transform的代码。//packages/compiler-core/src/transform.tsexportfunctiontransform(root,options){constcontext=createTransformContext(root,options)traverseNode(root,context)}exportfunctiontraverseNode(node,context){context.currentNode=nodeconst{nodeTransforms}=contextconstexitFns=[]for(leti=0;i
{//transformElement不执行任何逻辑,直接返回一个退出函数//说明transformElement需要等待所有的子节点处理完才可以执行返回函数postTransformElement(){const{tag,props}=nodeletvnodePropsletvnodePatchFlagconstvnodeTag=node.tagType===ElementTypes.COMPONENT?resolveComponentType(node,context):`"${tag}"`letpatchFlag=0//检测节点属性if(props.length>0){//检测节点属性的动态部分constpropsBuildResult=buildProps(node,context)vnodeProps=propsBuildResult.propspatchFlag=propsBuildResult.patchFlag}//检测子节点if(node.children.length>0){if(node.children.length===1){constchild=node.children[0]//检测子节点nodes是否是动态文本if(!getStaticType(child)){patchFlag|=PatchFlags.TEXT}}}//格式化patchFlagif(patchFlag!==0){vnodePatchFlag=String(patchFlag)}node.codegenNode=createVNodeCall(context,vnodeTag,vnodeProps,vnodeChildren,vnodePatchFlag)}}buildProps会遍历一次节点的属性。由于内部源码涉及很多其他细节,这里的代码进行了简化,只保留了patchFlag相关的逻辑。exportfunctionbuildProps(node:ElementNode,context:TransformContext,props:ElementNode['props']=node.props){letpatchFlag=0for(leti=0;iname:{{name}}节点包含变量类属性(CLASS=1<<1)