1.前言今天看到一道面试题,挺有意思的。做一些研究并报告你学到的东西。consttmp=`
{{person.name}}
金钱:{{person.money}}母亲:{{parents[1]}}`//需要写渲染函数consthtml=render(tmp,{person:{name:'petter',money:'10w',},parents:['Mrjack','Mrslucy']});//预期输出constexpect=`petter
money:100w
mother:Mrslucy
`2.简单模板编译2.1思路1:定期替换1.首先遍历数据找到所有的值constval={'person.name':'petter','person.money':'100w','person.parents[0]':'Mrjack''person.parents[1]':'Mrslucy'}2.遍历val,如果模板中有val,全局替换有两个问题。一是数组不好处理。二是层级不好处理。层次越深,性能越差。2.2思路二:newFunction+with1。先把所有的大胡子语法转换成一个标准的字符串模板2.像这样使用newFunction('with(data){returnconvertedtemplate}')在模板中可以直接使用${person.money}这样的数据,不需要额外转换constrender=(tmp,data)=>{constgenCode=(temp)=>{constreg=/\{\{(\S+)\}\}/greturntemp.replace(reg,function(...res){return'${'+res[1]+'}'})}constcode=genCode(tmp)constfn=newFunction('data',`with(data){return\`${code}\`}`)returnfn(data)}我们来看看fn函数的效果//console.log(fn.toString())functionanonymous(data){with(data){return`${person.name}
money:${person.money}mother:${parents[1]}`}}这么好的解决方案Solution1的问题3.逻辑高级编译面试一般不会涉及逻辑文法,但是需要知道逻辑文法的处理思路。逻辑不能直接用常规替换来处理。我们只能用正则表达式来匹配这段逻辑。然后在语法框架下单独写方法处理逻辑。所以我们首先需要得到语法框架,也就是所谓的AST。是具体描述语法结构的对象//比如当前模板consttmp=`chooseoneperson
person2.money">{{person1.name}}{{person2.name}}//Dataconstobj={person1:{money:1000,name:'高帅穷人'},person2:{money:100000,name:'矮丑富'},}//resultletres=render(tmp,obj)console.log(res)//选择一个人
矮丑富`基本思路:1.使用正则匹配得到AST2。使用AST拼写字符串(字符串中有一些方法可以产生你想要的结果,需要提前定义)3.newfunction+with生成render函数4.传递参数并执行render3.1生成astdefineast中节点的结构.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){letreg=/\{\{(\S+)\}\}/if(reg.test(text)){返回文本.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}3.2匹配正则执行响应回调得到ast我这里写的正则是每次匹配都是一行闭合标签。如果匹配到,就会触发相应的方法,并转化为一个节点存入ast中,每次处理完数组中的一行,从tmep中截取,然后处理下一行,直到处理完成constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?务必添加非贪婪模式,否则它将匹配下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)看一下得到的ast[Node{id:1,tag:'h1',text:"'选择一个人'",attrs:'',elseFlag:false,ifFlag:false,ifExp:''},Node{id:2,tag:'div',text:'person1.name',attrs:'#if="person1.money>person2.money"',elseFlag:false,ifFlag:true,ifExp:'person1.money>person2.money'},节点{id:3,tag:'div',text:'person2.name',attrs:'#else',elseFlag:true,ifFlag:false,ifExp:''}]3.2拼接字符串开始拼字符串constgenCode=(ast)=>{letstr=''for(vari=0;iperson2.money)?_c('div',person1.name):_c('div',person2.name);3.3生成渲染函数并执行函数render(){//...constfn=newFunction('data',`with(data){letstr='';${code}returnstr}`)returnfn(data)}让我们看看最终的fn函数//console.log(fn.toString())functionanonymous(data){with(data){letstr='';str+=_c('h1','选择一个人');str+=(person1.money>person2.money)?_c('div',person1.name):_c('div',person2.name);returnstr}}让我们定义另一个接下来_c,advanceconstcreatEle=(type,text)=>`<${type}>${text}${type}>`data._c=creatEle//这个很重要,因为_c其实就是读它是with中data参数的_c,必须赋值constadvance=(temp,n)=>{returntemp.substring(n)}3.4完整代码consttmp=`chooseoneperson
person2.money">{{person1.name}}{{person2.name}}`letid=1classNode{constructor(tag,attrs,text){this.id=id++this.tag=tagthis.text=this.handleText(text)this.attrs=attrsthis.elseFlag=falsethis.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){让reg=/\{\{(\S+)\}\}/if(reg.test(text)){returntext.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}constrender=(temp,data)=>{constcreatEle=(type,text)=>`<${type}>${text}${type}>`data._c=creatEleconstadvance=(temp,n)=>{returntemp.substring(n)}constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?一定要加非贪心模式或者它将匹配到下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)constgenCode=(ast)=>{让str=''for(vari=0;iperson2.money)?_c('div',person1.name):_c('div',person2.name);constfn=newFunction('data',`with(data){letstr='';${code}returnstr}`)console.log(fn.toString())returnfn(data)}constobj={person1:{money:1000,name:'高帅穷'},person2:{money:100000,name:'穷丑富'},}letres=render(tmp,obj)console.log(res)//<h1>选一个人矮丑富3.5优点和需要改进的地方首先肯定模板编译是大家一起做的,处理模板=>生成ast=>生成render函数=>传参执行函数的好处:由于模板不会变,一般数据会变,所以只需要编译一次,可以反复使用。局限性:这里说的局限性指的是我写的方法的局限性,1.由于正则表达式是专门为这道题写的,如果模板格式改变,正则表达式将不会生效。根本原因是我的正则表达式匹配一行标签中的所有内容。我的认知是,比赛越多,情况越复杂,越容易出错。2、node和if逻辑的实现比较简单。改进:正则化可以参考vue中的实现,匹配强度为开始注和结束注。从而区分是属性还是标签还是文字。具体可以看vue中的实现。四、一些应用1.pug也是template编译成ast生成render然后newFunction。没用with,但是实现了类似的方法,参数一个一个传入。感觉不是特别好。constpug=require('哈巴狗');constpath=require('path')constcompiledFunction=pug.compile('p#{name1}的Pug代码,用于调试#{obj}');//console.log(compiledFunction.toString())console.log(compiledFunction({name1:'fyy',obj:'compiler'}));//查看编译后的函数//functiontemplate(locals){//varpug_html=""//varlocals_for_with=(locals||{});//(function(name1,obj){//pug_html=pug_html+"\u003Cp\u003E";//p标签//pug_html=pug_html+pug.escape(name1);//pug_html=pug_html+"用于调试的Pug代码";//pug_html=pug_html+pug.escape(obj)+"\u003C\u002Fp\u003E";//}.call(this,locals_for_with.name1,locals_for_with.obj));//returnpug_html;//}附上调试关键图片返回newFunction的功能。看看compileBody里面有什么。原来ast生成了。看它的ast,长这个样子看一下根据ast生成的字符串函数2.Vuevue后面会写一篇文章来讲解,先看一下//htmlgt;{{name}}
petter
money:100w
mother:Mrslucy
`2.简单模板编译2.1思路1:定期替换1.首先遍历数据找到所有的值constval={'person.name':'petter','person.money':'100w','person.parents[0]':'Mrjack''person.parents[1]':'Mrslucy'}2.遍历val,如果模板中有val,全局替换有两个问题。一是数组不好处理。二是层级不好处理。层次越深,性能越差。2.2思路二:newFunction+with1。先把所有的大胡子语法转换成一个标准的字符串模板2.像这样使用newFunction('with(data){returnconvertedtemplate}')在模板中可以直接使用${person.money}这样的数据,不需要额外转换constrender=(tmp,data)=>{constgenCode=(temp)=>{constreg=/\{\{(\S+)\}\}/greturntemp.replace(reg,function(...res){return'${'+res[1]+'}'})}constcode=genCode(tmp)constfn=newFunction('data',`with(data){return\`${code}\`}`)returnfn(data)}我们来看看fn函数的效果//console.log(fn.toString())functionanonymous(data){with(data){return`${person.name}
money:${person.money}mother:${parents[1]}`}}这么好的解决方案Solution1的问题3.逻辑高级编译面试一般不会涉及逻辑文法,但是需要知道逻辑文法的处理思路。逻辑不能直接用常规替换来处理。我们只能用正则表达式来匹配这段逻辑。然后在语法框架下单独写方法处理逻辑。所以我们首先需要得到语法框架,也就是所谓的AST。是具体描述语法结构的对象//比如当前模板consttmp=`chooseoneperson
person2.money">{{person1.name}}{{person2.name}}//Dataconstobj={person1:{money:1000,name:'高帅穷人'},person2:{money:100000,name:'矮丑富'},}//resultletres=render(tmp,obj)console.log(res)//选择一个人
矮丑富`基本思路:1.使用正则匹配得到AST2。使用AST拼写字符串(字符串中有一些方法可以产生你想要的结果,需要提前定义)3.newfunction+with生成render函数4.传递参数并执行render3.1生成astdefineast中节点的结构.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){letreg=/\{\{(\S+)\}\}/if(reg.test(text)){返回文本.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}3.2匹配正则执行响应回调得到ast我这里写的正则是每次匹配都是一行闭合标签。如果匹配到,就会触发相应的方法,并转化为一个节点存入ast中,每次处理完数组中的一行,从tmep中截取,然后处理下一行,直到处理完成constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?务必添加非贪婪模式,否则它将匹配下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)看一下得到的ast[Node{id:1,tag:'h1',text:"'选择一个人'",attrs:'',elseFlag:false,ifFlag:false,ifExp:''},Node{id:2,tag:'div',text:'person1.name',attrs:'#if="person1.money>person2.money"',elseFlag:false,ifFlag:true,ifExp:'person1.money>person2.money'},节点{id:3,tag:'div',text:'person2.name',attrs:'#else',elseFlag:true,ifFlag:false,ifExp:''}]3.2拼接字符串开始拼字符串constgenCode=(ast)=>{letstr=''for(vari=0;iperson2.money)?_c('div',person1.name):_c('div',person2.name);3.3生成渲染函数并执行函数render(){//...constfn=newFunction('data',`with(data){letstr='';${code}returnstr}`)returnfn(data)}让我们看看最终的fn函数//console.log(fn.toString())functionanonymous(data){with(data){letstr='';str+=_c('h1','选择一个人');str+=(person1.money>person2.money)?_c('div',person1.name):_c('div',person2.name);returnstr}}让我们定义另一个接下来_c,advanceconstcreatEle=(type,text)=>`<${type}>${text}${type}>`data._c=creatEle//这个很重要,因为_c其实就是读它是with中data参数的_c,必须赋值constadvance=(temp,n)=>{returntemp.substring(n)}3.4完整代码consttmp=`chooseoneperson
person2.money">{{person1.name}}{{person2.name}}`letid=1classNode{constructor(tag,attrs,text){this.id=id++this.tag=tagthis.text=this.handleText(text)this.attrs=attrsthis.elseFlag=falsethis.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){让reg=/\{\{(\S+)\}\}/if(reg.test(text)){returntext.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}constrender=(temp,data)=>{constcreatEle=(type,text)=>`<${type}>${text}${type}>`data._c=creatEleconstadvance=(temp,n)=>{returntemp.substring(n)}constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?一定要加非贪心模式或者它将匹配到下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)constgenCode=(ast)=>{让str=''for(vari=0;iperson2.money)?_c('div',person1.name):_c('div',person2.name);constfn=newFunction('data',`with(data){letstr='';${code}returnstr}`)console.log(fn.toString())returnfn(data)}constobj={person1:{money:1000,name:'高帅穷'},person2:{money:100000,name:'穷丑富'},}letres=render(tmp,obj)console.log(res)//<h1>选一个人矮丑富3.5优点和需要改进的地方首先肯定模板编译是大家一起做的,处理模板=>生成ast=>生成render函数=>传参执行函数的好处:由于模板不会变,一般数据会变,所以只需要编译一次,可以反复使用。局限性:这里说的局限性指的是我写的方法的局限性,1.由于正则表达式是专门为这道题写的,如果模板格式改变,正则表达式将不会生效。根本原因是我的正则表达式匹配一行标签中的所有内容。我的认知是,比赛越多,情况越复杂,越容易出错。2、node和if逻辑的实现比较简单。改进:正则化可以参考vue中的实现,匹配强度为开始注和结束注。从而区分是属性还是标签还是文字。具体可以看vue中的实现。四、一些应用1.pug也是template编译成ast生成render然后newFunction。没用with,但是实现了类似的方法,参数一个一个传入。感觉不是特别好。constpug=require('哈巴狗');constpath=require('path')constcompiledFunction=pug.compile('p#{name1}的Pug代码,用于调试#{obj}');//console.log(compiledFunction.toString())console.log(compiledFunction({name1:'fyy',obj:'compiler'}));//查看编译后的函数//functiontemplate(locals){//varpug_html=""//varlocals_for_with=(locals||{});//(function(name1,obj){//pug_html=pug_html+"\u003Cp\u003E";//p标签//pug_html=pug_html+pug.escape(name1);//pug_html=pug_html+"用于调试的Pug代码";//pug_html=pug_html+pug.escape(obj)+"\u003C\u002Fp\u003E";//}.call(this,locals_for_with.name1,locals_for_with.obj));//returnpug_html;//}附上调试关键图片返回newFunction的功能。看看compileBody里面有什么。原来ast生成了。看它的ast,长这个样子看一下根据ast生成的字符串函数2.Vuevue后面会写一篇文章来讲解,先看一下//htmlgt;{{name}}
chooseoneperson
person2.money">{{person1.name}}
{{person2.name}}
//Dataconstobj={person1:{money:1000,name:'高帅穷人'},person2:{money:100000,name:'矮丑富'},}//resultletres=render(tmp,obj)console.log(res)//选择一个人
矮丑富
`基本思路:1.使用正则匹配得到AST2。使用AST拼写字符串(字符串中有一些方法可以产生你想要的结果,需要提前定义)3.newfunction+with生成render函数4.传递参数并执行render3.1生成astdefineast中节点的结构.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){letreg=/\{\{(\S+)\}\}/if(reg.test(text)){返回文本.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}3.2匹配正则执行响应回调得到ast我这里写的正则是每次匹配都是一行闭合标签。如果匹配到,就会触发相应的方法,并转化为一个节点存入ast中,每次处理完数组中的一行,从tmep中截取,然后处理下一行,直到处理完成constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?务必添加非贪婪模式,否则它将匹配下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)看一下得到的ast[Node{id:1,tag:'h1',text:"'选择一个人'",attrs:'',elseFlag:false,ifFlag:false,ifExp:''},Node{id:2,tag:'div',text:'person1.name',attrs:'#if="person1.money>person2.money"',elseFlag:false,ifFlag:true,ifExp:'person1.money>person2.money'},节点{id:3,tag:'div',text:'person2.name',attrs:'#else',elseFlag:true,ifFlag:false,ifExp:''}]3.2拼接字符串开始拼字符串constgenCode=(ast)=>{letstr=''for(vari=0;ichooseoneperson
person2.money">{{person1.name}}
{{person2.name}}
`letid=1classNode{constructor(tag,attrs,text){this.id=id++this.tag=tagthis.text=this.handleText(text)this.attrs=attrsthis.elseFlag=falsethis.ifFlag=falsethis.ifExp=''this.handleAttrs()}handleText(text){让reg=/\{\{(\S+)\}\}/if(reg.test(text)){returntext.replace(reg,function(...res){returnres[1]})}else{return`\'${text}\'`}}handleAttrs(){constifReg=/#if=\"(\S+)\"/constelesReg=/#else/if(elesReg.test(this.attrs)){this.elseFlag=true}constres=this.attrs.match(ifReg)if(res){this.ifFlag=truethis.ifExp=res[1]}}}constrender=(temp,data)=>{constcreatEle=(type,text)=>`<${type}>${text}${type}>`data._c=creatEleconstadvance=(temp,n)=>{returntemp.substring(n)}constgenAST=(temp)=>{//只适用于没有文字的标签constroot=[]constblockreg=/(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)///?一定要加非贪心模式或者它将匹配到下一个标签while(temp){letblock=temp.match(blockreg)letnode=newNode(block[2],block[3],block[4])root.push(node)temp=advance(temp,block[1].length)}returnroot}constast=genAST(temp)console.log(ast)constgenCode=(ast)=>{让str=''for(vari=0;i矮丑富
3.5优点和需要改进的地方首先肯定模板编译是大家一起做的,处理模板=>生成ast=>生成render函数=>传参执行函数的好处:由于模板不会变,一般数据会变,所以只需要编译一次,可以反复使用。局限性:这里说的局限性指的是我写的方法的局限性,1.由于正则表达式是专门为这道题写的,如果模板格式改变,正则表达式将不会生效。根本原因是我的正则表达式匹配一行标签中的所有内容。我的认知是,比赛越多,情况越复杂,越容易出错。2、node和if逻辑的实现比较简单。改进:正则化可以参考vue中的实现,匹配强度为开始注和结束注。从而区分是属性还是标签还是文字。具体可以看vue中的实现。四、一些应用1.pug也是template编译成ast生成render然后newFunction。没用with,但是实现了类似的方法,参数一个一个传入。感觉不是特别好。constpug=require('哈巴狗');constpath=require('path')constcompiledFunction=pug.compile('p#{name1}的Pug代码,用于调试#{obj}');//console.log(compiledFunction.toString())console.log(compiledFunction({name1:'fyy',obj:'compiler'}));//查看编译后的函数//functiontemplate(locals){//varpug_html=""//varlocals_for_with=(locals||{});//(function(name1,obj){//pug_html=pug_html+"\u003Cp\u003E";//p标签//pug_html=pug_html+pug.escape(name1);//pug_html=pug_html+"用于调试的Pug代码";//pug_html=pug_html+pug.escape(obj)+"\u003C\u002Fp\u003E";//}.call(this,locals_for_with.name1,locals_for_with.obj));//returnpug_html;//}附上调试关键图片返回newFunction的功能。看看compileBody里面有什么。原来ast生成了。看它的ast,长这个样子看一下根据ast生成的字符串函数2.Vuevue后面会写一篇文章来讲解,先看一下//html