今天这篇文章打算学习Vue3和Vue2下模板编译的区别,以及VDOM下Diff算法的优化。在编译入口了解过Vue3的同学一定知道,Vue3引入了一个新的组合API。组件挂载阶段会调用setup方法,然后判断render方法是否存在。如果没有,将调用编译方法将模板转换为渲染。//packages/runtime-core/src/renderer.tsconstmountComponent=(initialVNode,container)=>{constinstance=(initialVNode.component=createComponentInstance(//...params))//调用设置setupComponent(instance)}//packages/runtime-core/src/component.tsletcompileexportfunctionregisterRuntimeCompiler(_compile){compile=_compile}exportfunctionsetupComponent(instance){constComponent=instance.typeconst{setup}=Componentif(setup){//...callsetup}if(compile&&Component.template&&!Component.render){//如果没有render方法//调用compile将template转换为render方法Component.render=compile(Component.template,{...})}}这部分是runtime-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,//Compilationconfigurationoptions?:CompilerOptions):RenderFunction{if(!isString(template)){//如果模板不是字符串//它被认为是DOM节点,获取innerHTMLif(template.nodeType){template=template.innerHTML}else{returnNOOP}}//如果缓存中存在,直接从缓存中获取constkey=templateconstcached=compileCache[key]if(cached){returncached}//如果存在是一个ID选择器,获取DOM元素后,取innerHTMLif(template[0]==='#'){constel=document.querySelector(template)template=el?el.innerHTML:''}//调用编译获取渲染代码const{code}=compile(template,options)//将渲染代码转换为函数constrender=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'从'./transforms/transformText'导入{transformElement}从'./transforms/transformElement'导入{transformOn}从'./transforms/vOn'导入{transformBind}从'./transforms/vBind'导入{transformModel}from'./transforms/vModel'exportfunctionbaseCompile(template,options){//解析html,转换为astconstast=baseParse(template,options)//优化ast,标记静态节点transform(ast,{...选项,节点transforms:[transformIf,transformFor,transformText,transformElement,//...省略部分transform],directiveTransforms:{on:transformOn,bind:transformBind,model:transformModel}})//将ast转换为可执行代码returngenerate(ast,options)}这里计算PatchFlag的大体逻辑和前面的没有太大区别。主要原因是optimize方法变成了transform方法,一些模板语法默认会进行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不执行ute任何逻辑,但是直接返回一个退出函数//DescriptiontransformElelement需要等待所有的子节点处理完毕后才执行returnfunctionpostTransformElement(){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]//检测子节点是否为动态Textif(!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){让patchFlag=0for(leti=0;i名称:{{name}}该节点包含一个可变类属性(CLASS=1<<1)