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是怎麼做這塊的