1.前言

明天看到一道面试题,挺有意思的。
钻研了一下,汇报一下学习所得。

const tmp = `<h1>{{person.name}}</h1><h2>money:{{person.money}}</h1><h3>mother:{{parents[1]}}</h1>`//须要编写render函数const html = render(tmp, {  person: {    name: 'petter',    money: '10w',  },  parents: ['Mr jack','Mrs lucy']});//冀望的输入const expect = `  <h1>petter</h1>  <h2>money:100w</h2>  <h2>mother:Mrs lucy</h2>`

2.简略模板编译

2.1思路一:正则替换

1.先遍历data找出所有的值

const val = { 'person.name': 'petter', 'person.money': '100w', 'person.parents[0]': 'Mr jack' 'person.parents[1]': 'Mrs lucy'}

2.遍历val,如果模板中有val,则全局替换

这样做有两个问题,一个是对象不好解决。第二个是层级不好解决。层级越深性能越差

2.2思路二:new Function + with

1.先把所有的大胡子语法转成规范的字符串模板
2.利用new Function(' with (data){return 转化后的模板 }')
这样模板中的就能够间接应用${person.money}这种数据不须要额定转化

const render = (tmp,data)=>{    const genCode = (temp)=>{        const reg = /\{\{(\S+)\}\}/g        return temp.replace(reg,function(...res){            return '${'+res[1]+'}'        })    }    const code = genCode(tmp)    const fn = new Function(                'data',`with(data){ return \`${code}\` }`)      return fn(data)}

咱们看一下fn函数的成果

//console.log(fn.toString())function anonymous(data) {    with(data){ return `        <h1>${person.name}</h1>        <h2>money:${person.money}</h1>        <h3>mother:${parents[1]}</h1>`         }    }

这样很好的解决的计划一的一些问题

3.带逻辑的高级编译

个别面试的时候不会带有逻辑语法,然而咱们须要晓得逻辑语法的解决思路。

逻辑没法用正则替换间接解决。咱们只能用正则去匹配到这一段逻辑。
而后在语法框架下独自去写办法去解决逻辑。
所以咱们首先须要拿到语法框架,也就是所谓的AST。它就是专门形容语法结构的一个对象

//比方当初的模板const tmp = `<h1>choose one person</h1><div #if="person1.money>person2.money">{{person1.name}}</div><div #else>{{person2.name}}</div>// 数据const obj = {    person1: {       money: 1000,       name: '高帅穷'    },    person2: {        money: 100000,        name: '矮丑富'     },}// 后果let res = render(tmp,obj)console.log(res) //<h1>choose one person</h1><div>矮丑富</div>`

基本思路:
1.利用正则匹配拿到AST
2.利用AST去拼字符串(字符串外面有一些办法,用来产出你所要的后果,须要提前定义好)
3.new function + with 去生成render函数
4.传参执行render

3.1 生成ast

定义一个ast中节点的构造

class Node {    constructor(tag,attrs,text){        this.id = id++        this.tag = tag        this.text = this.handleText(text)        this.attrs = attrs        this.elseFlag = false        this.ifFlag = false        this.ifExp = ''        this.handleAttrs()    }    handleText(text){        let reg = /\{\{(\S+)\}\}/        if(reg.test(text)){            return text.replace(reg,function(...res){                return res[1]            })        }else{            return `\'${text}\'`        }           }    handleAttrs(){        const ifReg = /#if=\"(\S+)\"/        const elesReg = /#else/        if(elesReg.test(this.attrs)){            this.elseFlag = true        }        const res = this.attrs.match(ifReg)        if(res){            this.ifFlag = true            this.ifExp = res[1]        }    }}

3.2 匹配正则 执行响应的回调 拿到ast

我这里写的正则是每次匹配的是一行闭合标签
如果匹配到则触发相应的办法,将其转化为一个节点存到ast数组里
每次解决完一行,则把它从tmep里剪掉,再解决下一行,晓得解决完

const genAST = (temp)=>{ //只实用标签间没有文本        const root = []        const blockreg =  /(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)/    // ?肯定要加 非贪心模式 否则会匹配到前面啷个标签        while(temp ){            let block = temp.match(blockreg)            let node = new Node(block[2],block[3],block[4])            root.push(node)            temp = advance(temp,block[1].length)        }        return root    }    const ast = genAST(temp)    console.log(ast) 

咱们看一下拿到的ast

[            Node {                id: 1,                tag: 'h1',                text: "'choose one person'",                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'        },        Node {            id: 3,            tag: 'div',            text: 'person2.name',            attrs: ' #else',            elseFlag: true,            ifFlag: false,            ifExp: ''        }    ]

3.2 拼字符串

上面开始拼字符串

const genCode = (ast)=>{        let str = ''        for(var i = 0;i<ast.length;i++){            let cur = ast[i]            if(!cur.ifFlag && !cur.elseFlag){                str+=`str+=_c('${cur.tag}',${cur.text});`            }else if(cur.ifFlag){                str+=`str+=(${cur.ifExp})?_c('${cur.tag}',${cur.text})`            }else if(cur.elseFlag){                str+=`:_c('${cur.tag}',${cur.text});`            }                            }        return str    }    const code = genCode(ast) 

咱们瞅一眼拼好的字符串

//  console.log('code:',code) // code: str+=_c('h1','choose one person');str+=(person1.money>person2.money)?_c('div',person1.name):_c('div',person2.name);

3.3 生成render函数并执行

function render(){    //...   const fn = new Function('data',`with(data){ let str = ''; ${code} return str }`)     return fn(data)}   

咱们瞅一眼最终的fn函数

// console.log(fn.toString())    function anonymous(data) {            with(data){                 let str = '';                 str+=_c('h1','choose one person');                str+=(person1.money>person2.money)?_c('div',person1.name):_c('div',person2.name);                 return str             }        }

咱们再定义一下_c,advance

 const creatEle=(type,text)=> `<${type}>${text}</${type}>`  data._c = creatEle //这里很重要 因为_c其实读的是with中data参数的_c,肯定要给赋值上 const advance = (temp,n)=>{        return temp.substring(n)    }

3.4 残缺代码

const tmp = `<h1>choose one person</h1><div #if="person1.money>person2.money">{{person1.name}}</div><div #else>{{person2.name}}</div>`let id = 1class Node {    constructor(tag,attrs,text){        this.id = id++        this.tag = tag        this.text = this.handleText(text)        this.attrs = attrs        this.elseFlag = false        this.ifFlag = false        this.ifExp = ''        this.handleAttrs()    }    handleText(text){        let reg = /\{\{(\S+)\}\}/        if(reg.test(text)){            return text.replace(reg,function(...res){                return res[1]            })        }else{            return `\'${text}\'`        }           }    handleAttrs(){        const ifReg = /#if=\"(\S+)\"/        const elesReg = /#else/        if(elesReg.test(this.attrs)){            this.elseFlag = true        }        const res = this.attrs.match(ifReg)        if(res){            this.ifFlag = true            this.ifExp = res[1]        }    }}const render = (temp,data)=>{    const creatEle=(type,text)=> `<${type}>${text}</${type}>`    data._c = creatEle    const advance = (temp,n)=>{        return temp.substring(n)    }        const genAST = (temp)=>{ //只实用标签间没有文本        const root = []        const blockreg =  /(\s*<(\w+)([^]*?)>([^>]*?)<\/\2>\s*)/    // ?肯定要加 非贪心模式 否则会匹配到前面啷个标签        while(temp ){            let block = temp.match(blockreg)            let node = new Node(block[2],block[3],block[4])            root.push(node)            temp = advance(temp,block[1].length)        }        return root    }    const ast = genAST(temp)    console.log(ast)         const genCode = (ast)=>{        let str = ''        for(var i = 0;i<ast.length;i++){            let cur = ast[i]            if(!cur.ifFlag && !cur.elseFlag){                str+=`str+=_c('${cur.tag}',${cur.text});`            }else if(cur.ifFlag){                str+=`str+=(${cur.ifExp})?_c('${cur.tag}',${cur.text})`            }else if(cur.elseFlag){                str+=`:_c('${cur.tag}',${cur.text});`            }                            }        return str    }    const code = genCode(ast)    console.log('code:',code) // code: str+=_c('h1','choose one person');str+=(person1.money>person2.money)?_c('div',person1.name):_c('div',person2.name);        const fn = new Function('data',`with(data){ let str = ''; ${code} return str }`)      console.log(fn.toString())            return fn(data)}const obj = {    person1: {       money: 1000,       name: '高帅穷'    },    person2: {        money: 100000,        name: '矮丑富'     },}let res = render(tmp,obj)console.log(res) //<h1>choose one person</h1><div>矮丑富</div>

3.5 长处与待改良点

首先能够必定,模板编译大家都是这么做的,解决模板=>生成ast=>生成render函数=>传参执行函数

益处: 因为模板不会变,个别都是data变,所以只须要编译一次,就能够重复应用
局限性: 这里说的局限性是指我写的办法的局限性,
1.因为正则是专门为这道题写的,所以模板格局换一换就正则就不失效了。根本原因是我的正则匹配的是相似一行标签外面的所有货色。我的感悟是匹配的越多,状况越简单,越容易出问题。
2.node实现和if逻辑的实现上比拟简陋
改良点: 对于正则能够参考vue中的实现,匹配力度为开始便签完结便签。从而辨别是属性还是标签还是文本。具体能够看下vue中的实现。

4.一些利用

1.pug

也是模板编译成ast生成render而后再new Function,没用with,然而实现了一个相似的办法,把参数一个个传进去了,感觉不是特地好

const pug = require('pug');const path = require('path')const compiledFunction = pug.compile('p #{name1}的 Pug 代码,用来调试#{obj}');// console.log(compiledFunction.toString())console.log(compiledFunction({    name1: 'fyy',    obj: 'compiler'}));//看一下编译出的函数// function template(locals) {//     var pug_html = ""//     var locals_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));//     return pug_html;// }

附上调试的要害图
返回的是new Function的函数

看下compileBody外面有啥 ,原来是生成了ast,看下它的ast原来是长这屌样

再看一下依据ast生成的字符串函数

2.Vue

vue的话前面会写文章细说,先简略看看

 //html <div id="app" a=1 style="color:red;background:lightblue">        <li b="1">{{name}}</li> </div>//scriptlet vm = new Vue({            data() {                return {                    name:'fyy'                }            },        });        vm.$mount('#app')

咱们看下这段代码是怎么编译的

function compileToFunction(template) {    let root = parserHTML(template) //ast    // 生成代码    let code = generate(root)    console.log(code)    // _c('div',{id:"app",a:"1",style:{"color":"red","background":"lightblue"}},_c('li',{b:"1"},_v(_s(name))))   //name取的是this上的name    let render = new Function(`with(this){return ${code}}`); // code 中会用到数据 数据在vm上    return render;    // html=> ast(只能形容语法 语法不存在的属性无奈形容) => render函数 + (with + new Function) => 虚构dom (减少额定的属性) => 生成实在dom}

5.总结

总的来说,感觉模板编译就是正则匹配生成ast+依据逻辑拼字符串函数的一个过程,当然难点也就在这两个中央。
万幸,个别面试预计只会出的2.2的难度。本文章知识点应该是能齐全笼罩的。如果不写框架的话,懂这些应该够用了。
前面的文章会具体分析下vue是怎么做这块的