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

学习vue源码(九)手写代码生成器

时间:2023-04-01 01:58:00 vue.js

早点学习vue源码(六)熟悉模板编译原理我们讲到模板编译分为解析器、优化器、代码生成器。前两部分已经实现了,下面看看代码生成器是如何实现的。代码生成器的工作是使用AST生成渲染函数代码字符串。解析器主要做的事情是将模板字符串转换成元素AST,例如:像这样:{tag:"div"type:1,staticRoot:false,static:false,plain:true,parent:undefined,attrsList:[],attrsMap:{},children:[{tag:"p"type:1、staticRoot:false,static:false,plain:true,parent:{tag:"div",...},attrsList:[],attrsMap:{},children:[{type:2,text:"{{name}}",static:false,expression:"_s(name)"}]}]}使用示例中模板生成的AST生成render是这样的:{render:`with(this){return_c('div',[_c('p',[_v(_s(name))])])}`}的格式如下:with(this){return_c('div',[_c('p',[_v(_s(name))]])}在生成的代码字符串中,我们可以看到有几个函数调用_c、_v、_s。_c对应createElement,用于创建元素。第一个参数是HTML标记名称。第二个参数是与元素上使用的属性对应的数据对象。第三个参数是可选的。第三个参数是children。例如:一个简单的模板:1

生成的代码字符串为:`with(this){return_c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])}`格式化后:with(this){return_c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])}_v表示创建一个文本节点。_s是返回参数中的字符串。可能有的同学会觉得格式化后的代码很奇怪。其实去掉with之后就很熟悉了(其实with是用来改变作用域的,去掉不影响我们的理解)return_c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])你觉得这熟悉吗?没错,我们就上vue官网的render函数。bj.ufileos.c...]有没有发现生成的代码字符串其实就是vue官网介绍的render函数中的createElement函数。而参数也是对应的第一个参数:标签名和第二个参数:节点数据的第三个参数:子节点数组代码生成器的整体逻辑其实就是用elementASTs进行递归,然后拼出这样的_c('div',[_c('p',[_v(_s(name))])])字符串。那么这个字符串怎么拼呢??请看下面的代码:functiongenElement(el:ASTElement,state:CodegenState){constdata=el.plain?undefined:genData(el,state)constchildren=genChildren(el,state,true)letcode=`_c('${el.tag}'${data?`,${data}`:''//数据}${children?`,${children}`:''//children})`returncode}因为_c的参数需要tagName、data和children。所以上面代码的主要逻辑是使用genData和genChildren获取data和children,然后放入_c中,完成后返回拼写的“_c(tagName,data,children)”。el.plain为真,节点没有属性。因此没有必要实施genData。所以我们现在比较关心两个问题:数据是怎么产生的(genData的实现逻辑)?children是如何生成的(genChildren的实现逻辑)?我们先看看genData是如何实现逻辑的:key},`}//refif(el.ref){data+=`ref:${el.ref},`}if(el.refInFor){data+=`refInFor:true,`}//preif(el.pre){data+=`pre:true,`}//...类似的情况还有很多data=data.replace(/,$/,'')+'}'returndata}正如你可以看到,它是根据当前节点在AST上的属性,然后针对不同的属性做一些不同的处理,最后拼出一个字符串~然后我们就看看genChildren是如何实现的:functiongenChildren(el:ASTElement,状态:代码生成状态):字符串|void{constchildren=el.childrenif(children.length){return`[${children.map(c=>genNode(c,state)).join(',')}]`}}functiongenNode(node:ASTNode,state:CodegenState):字符串{if(node.type===1){returngenElement(node,state)}if(node.type===3&&node.isComment){returngenComment(node)}否则{返回urngenText(node)}}从上面的代码可以看出,生成children的过程其实就是在AST中循环遍历当前节点的children,然后根据不同的节点类型重新执行每一项。genElementgenCommentgenText如果genElement在循环中有children在生成,这样重复递归,最后一圈后可以得到一个完整的render函数代码串,类似下面。"_c('div',[_c('p',[_v(_s(name))])])"最后将生成的代码放入with中。exportfunctiongenerate(ast:ASTElement|void,options:CompilerOptions):CodegenResult{conststate=newCodegenState(options)//如果ast为空,创建一个空的divconstcode=ast?genElement(ast,state):'_c("div")'return{render:`with(this){return${code}}`}}关于代码生成器的部分到此结束。其实源码远不止这么简单,我也没有太多细节去讲,只讲了一个大概的流程。对具体细节感兴趣的同学可以去源码了解详情。