1 Introduction
I saw an interview question today, which is quite interesting.
I studied it and reported what I learned.
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. Simple template compilation
2.1 Idea 1: Regular replacement
1. First traverse the data to find all the values
const val = {
'person.name': 'petter',
'person.money': '100w',
'person.parents[0]': 'Mr jack'
'person.parents[1]': 'Mrs lucy'
}
2. Traverse the val, if there is a val in the template, replace it globally
There are two problems with this, one is that the array is not easy to handle. The second is that the hierarchy is not easy to handle. The deeper the level, the worse the performance
2.2 Idea 2: new Function + with
1. First convert all the mustache grammar into a standard string template
2. Use new Function(' with (data){return converted template}')
In this way, the data of ${person.money} can be used directly in the template without additional conversion
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)
}
Let's take a look at the effect of the fn function
//console.log(fn.toString())
function anonymous(data) {
with(data){ return `
<h1>${person.name}</h1>
<h2>money:${person.money}</h1>
<h3>mother:${parents[1]}</h1>`
}
}
Some of the problems of such a good solution
3. Advanced compilation with logic
In general interviews, there will be no logical grammar, but we need to know the processing ideas of logical grammar.
Logic cannot be directly processed with regular replacement. We can only use regularity to match this piece of logic.
Then write methods to process logic separately under the grammar framework.
So we first need to get the grammar framework, which is the so-called AST. It is an object that specifically describes the grammatical structure
//比如现在的模板
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>
`
The basic idea:
1. Use regular matching to get the AST
2. Use AST to spell strings ( string, which are used to produce the results you want, which need to be defined in advance)
3.new function + with to generate render function
4. Pass parameters to execute render
3.1 Generate ast
Define the structure of a node in an 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 The callback that matches the regular execution response gets ast
I write here is that each match is a line of closed tags
If it matches, trigger the corresponding method, convert it into a node and store it in the ast array
Every time a line is processed, cut it out of tmep, and then process the next line, until the process is complete
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)
Let's take a look at the ast we got
[
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 Spelling strings
Start spelling strings below
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)
Let's take a look at the spelled string
// 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 Generate and execute the render function
function render(){
//...
const fn = new Function('data',`with(data){ let str = ''; ${code} return str }`)
return fn(data)
}
Let’s take a look at the final fn function
// 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
}
}
let's define _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 Complete code
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 Advantages and points for improvement
First of all, you can be sure that template compilation is done by everyone. Processing template=>generate ast=>generate render function=>pass parameter execution function
Benefits: Since the template does not change, generally data changes, so you only need to compile it once and you can use it repeatedly
Limitations: The limitations mentioned here refer to the limitations of the method I wrote,
1. Since the regularity is written specifically for this inscription, the regularization will not take effect if the template format is changed. The root cause is that my regular match is similar to everything in a line of tags. My perception is that the more matches, the more complicated the situation and the easier it is to go wrong.
2. The implementation of node and if logic is relatively simple Improvement points: For regularity, you can refer to the implementation in vue, and the matching strength is the start note and the end note. In order to distinguish whether it is an attribute, a label, or a text. See the implementation in vue for details.
4. Some applications
1.pug
The template is also compiled into ast to generate render and then new Function, useless with, but a similar method is implemented, and the parameters are passed in one by one. It feels not particularly good
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;
// }
Attach the key diagram for debugging returns the function of new Function
at what's in the compileBody. It turns out that the ast is generated. Look at the ast that looks like this.
look at the string function generated by ast
2.Vue
I will write an article in detail later on vue, let’s take a brief look
//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')
Let's see how this code is compiled
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. Summary
In general, I feel that template compilation is a regular matching to generate ast+ according to the logic of the string function, of course, the difficulty lies in these two places.
Fortunately, the average interview is estimated to be only 2.2 difficulty. The knowledge points of this article should be completely covered. If you don't write a framework, it should be enough to understand these.
The following article will specifically analyze how Vue does this.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。