关于javascript:前端面试模板编译

20次阅读

共计 7430 个字符,预计需要花费 19 分钟才能阅读完成。

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 = 1
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]
        }
    }
}
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>
//script
let 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 是怎么做这块的

正文完
 0