Vue是前端从业人员绕不开的一个框架,随着学习的深入不免要通过观看Vue的源码深入学习,但是直接看Vue的源码并不是一件简单的事情,我们不如把Vue的各部分内容拆开来一点一点学习,由浅入深。
Vue的模板是一个很方便很受人喜欢的功能,mustache是一个比较老牌的模板引擎,跟Vue有很多相似之处,所以不妨先从mustache学起。
前排说明,关于mustache的内容是通过尚硅谷的一名讲师的课程学习的,感觉老师的教导。
模板引擎就是将数据变成视图的一种优雅的解决方案。
mustache基本用法
在学习mustache之前,我们要先了解mustache应该怎么用。
接下来给出几个小例子来简单说下用法,熟悉Vue的同学应该会感觉很亲切。
在md的代码块中写模板字符串,上传之后解析的格式有问题,大家可以复制放到编辑器中看。
基础应用: 渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <body> <div id="container"></div> <script type="module"> import mustache from './mustache.js' let templateStr = ` <h1>我买了新出的{{phone}}!</h1> ` let data = { phone: 'Iphone 13' } let domStr = mustache.render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
基础应用:简单循环
1 2 3
| 循环通过{{ #循环的数组 }} {{/循环的数组}} 来指定要循环的数组 简单循环内部通过 {{ . }} 来指代循环拿到的内容 分别对应Vue中的v-for指令和循环的item
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <body> <div id="container"></div> <script type="module"> import mustache from './mustache.js' let templateStr = ` <ul> {{#arr}} <li>{{.}}</li> {{/arr}} </ul> ` let data = { arr: ['苹果', '三星', '华为'] } let domStr = mustache.render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
基础应用: 循环
1 2
| 可以直接使用循环内容中的属性 对应Vue中的 item.key 来拿到循环内容
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <body> <div id="container"></div> <script type="module"> import mustache from './mustache.js' // 模板 let templateStr = ` <ul> {{#arr}} <li> <p>姓名: {{name}}</p> <p>年龄: {{age}}</p> <p>性别: {{gender}}</p> </li> {{/arr}} </ul> ` // 数据 let data = { arr: [ {name: '小明', age: 12, gender: 'male'}, {name: '小红', age: 13, gender: 'femail'} ] } // 通过模板和数据生成最终视图 let domStr = mustache.render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
基础应用:嵌套数组循环
1 2
| 在循环中在嵌套一层循环 对应Vue中 v-for 内部可以继续使用 v-for
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <body> <div id="container"></div> <script type="module"> import mustache from './mustache.js' let templateStr = ` <ul> {{#arr}} <li> <p>姓名: {{name}}</p> <p>年龄: {{age}}</p> <p> 爱好: {{#hobbies}} <span>{{.}}</span> {{/hobbies}} </p> </li> {{/arr}} </ul> ` let data = { arr: [ {'name': '小明', 'age': 22, 'hobbies': ['游泳', '羽毛球']}, {'name': '小明', 'age': 22, 'hobbies': ['游泳', '羽毛球']}, {'name': '小明', 'age': 22, 'hobbies': ['游泳', '羽毛球']}, ] } let domStr = mustache.render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
基础应用:布尔值
1 2 3
| 通过 {{#bool}}{{/bool}} 的方式,来根据布尔值决定是否需要显示内部包裹的内容 对应于 Vue 的 v-show 不过需要注意的是,mustach必须直接给定一个布尔值,而不能给一个算术运算,即便运算结果是一个布尔值,mustache并不会进行运算
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <body> <div id="container"></div> <script type="module"> import mustache from './mustache.js' let templateStr = ` <ul> {{#show}} <h1>我可以根据布尔值来显示和隐藏哦</h1> {{/show}} </ul> ` let data = { show: true } let domStr = mustache.render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
mustache的底层思想
我在学习mustache的时候,第一个想到的就是通过正则表达式来实现,然而mustache库是不能用简单的正则表达式来实现的,而是通过将模板字符串转译为tokens,然后结合数据生成dom字符串实现。
正则表达式示例
正则表达式其实是可以实现一个比较简单的模板引擎的,但是比较完善的模板引擎肯定是不合适的。
在此就给出一个错误示例了,下面的内容只是通过正则表达式实现一个简单的渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <body> <div id="container"></div> <script type="module"> let templateStr = `<h1>我买了一个{{thing}},好{{mood}}啊</h1>` let data = { thing: 'Iphone 13', mood: '开心' } let reg = /\{\{(\w+)\}\}/ function render(templateStr, data){ return templateStr.replace(reg, function(_, str, c ,d){ return data[str] }) } let domStr = render(templateStr, data) document.getElementById('container').innerHTML = domStr </script> </body>
|
真正的核心: tokens
tokens是一个JS的嵌套数组,说白了就是模板字符串的Js表示
我们熟知的抽象语法树、虚拟节点也是学习了tokens的理念
假设我们有一个模板字符串
1
| <h1>我买了一个{{thing}},好{{mood}}啊</h1>
|
mustache会将其解析为tokens,形式如下:
1 2 3 4 5 6 7
| tokens = [ ["text", "<h1>我买了一个"], ["name", "thing"], ["text", ",好"], ["name", "mood"], ["text", "啊</h1>"] ]
|
当然这只是简单模板的情况,如果模板字符串更为复杂,比如有循环存在的时候,就会被编译为有更深层嵌套的tokens
假设我们有如下的模板字符串:
1 2 3 4 5 6 7
| <div> <ul> {{#arr}} <li>{{.}}</li> {{/arr}} </ul> </div>
|
由于模板中有循环的存在,所以tokens也会有更深层的嵌套
1 2 3 4 5 6 7 8 9
| tokens = [ ["text", "<div><ul>"], ["#", "arr", [ ["text", "<li>"], ["name", "."], ["text", "</li>"] ]], ["text", "</ul></div>"] ]
|
如果有双重循环呢?那么tokens的嵌套层次也随之更深一层。
我们知道了mustache会把模板字符串编译为tokens,但这远远不够,mustache还会将tokens结合数据解析为dom字符串,这个我们后续再说。
总结一下,mustache库底层主要做了两件事情:
- 将模板字符串编译为tokens形式
- 将tokens结合数据,解析为dom字符串
手写mustache库的简单实现
Scanner类
Scanner类用于扫描模板字符串
我们模仿mustache的用法,自己实现一个MyTemplate。我们的html内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <body> <script src="./src/main.js"></script> <script> let templateStr = "我买了一个{{thing}},好{{mood}}啊" let data = { thing: '手机', mood: '开心' } MyTemplate.render(templateStr, data) </script> </body>
|
声明Scanner类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
export default class Scanner{ constructor(templateStr) { console.log('我是Scanner类',templateStr) this.templateStr = templateStr this.pos = 0 this.tail = templateStr }
scan(tag){ if (this.tail.indexOf(tag) == 0){ this.pos += tag.length this.tail = this.templateStr.substring(this.pos) } }
scanUntil(stopTag){ let pos_backup = this.pos while (this.eos() && this.tail.indexOf(stopTag) != 0){ this.pos++ this.tail = this.templateStr.substring(this.pos) } return this.templateStr.substring(pos_backup, this.pos) }
eos() { return this.pos < this.templateStr.length } }
|
main.js是我们的js程序入口文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
import Scanner from './Scanner.js'
window.MyTemplate = { render(templateStr, data){ let scanner = new Scanner(templateStr) while (scanner.pos != templateStr.length){ let words = scanner.scanUntil('{{') console.log(words) scanner.scan('{{') words = scanner.scanUntil('}}') console.log(words) scanner.scan('}}') } } }
|
将html变为tokens
当然接下来的内容还仅限于非嵌套的内容
我们不再通过main.js来调用Scanner,而是通过一个新的方法来实现
Scanner类不做修改,main.js移除对Scanner的调用
1 2 3 4 5 6 7 8 9 10 11 12
|
import parseTemplateToTokens from './parseTemplateToTokens.js'
window.MyTemplate = { render(templateStr, data){ let tokens = parseTemplateToTokens(templateStr) console.log(tokens) } }
|
我们用于将Html变为tokens的新函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
import Scanner from './Scanner.js' export default function parseTemplateToTokens(templateStr){ let tokens = []; let words; let scanner = new Scanner(templateStr) while(scanner.eos()){ words = scanner.scanUntil('{{') if(words != ''){ tokens.push(['text', words]) } scanner.scan('{{') words = scanner.scanUntil('}}') if(words != ''){ if(words[0] == '#'){ tokens.push(['#', words.substring(1)]) }else if (words[0] == '/'){ tokens.push(['/', words.substring(1)]) }else{ tokens.push(['name', words]) } } scanner.scan('}}') } return tokens; }
|
嵌套数组的形式
截止到仙子啊,我们的parseTemplateToTokens方法仅仅将所有内容收集,但是并没有将循环内部的内容生成嵌套数组,所以我们需要对其进行一些修改。
parseTemplateToTokens不再直接返回tokens,而是返回nestTokens(tokens)
真正处理完成的tokens由nestTokens函数返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js';
export default function parseTemplateToTokens(templateStr){ let tokens = []; let words; let scanner = new Scanner(templateStr) while(scanner.eos()){ words = scanner.scanUntil('{{') if(words != ''){ tokens.push(['text', words]) } scanner.scan('{{') words = scanner.scanUntil('}}') if(words != ''){ if(words[0] == '#'){ tokens.push(['#', words.substring(1)]) }else if (words[0] == '/'){ tokens.push(['/', words.substring(1)]) }else{ tokens.push(['name', words]) } } scanner.scan('}}') } return nestTokens(tokens); }
|
而我们在nestTokens中使用栈的数据结构来方便的收集循环内部的内容,实现嵌套数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
export default function nestTokens(tokens){ let nestedTokens = [] let sections = [] let collector = nestedTokens
for(let i=0; i<tokens.length; i++){ let token = tokens[i] switch(token[0]){ case '#': collector.push(token) sections.push(token) collector = token[2] = [] break; case '/': sections.pop() collector = sections.length > 0 ? sections[sections.length-1][2] : nestedTokens break; default: collector.push(token)
} } return nestedTokens; }
|
lookup函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
export default function lookup(dataObj, keyName){ if (keyName.indexOf('.') != -1){ let temp = dataObj let names = keyName.split('.'); for(let i=0; i<names.length; i++){ temp = temp[names[i]] } return temp } return dataObj[keyName]
}
|
renderTemplate函数用于将数组变为dom字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
import lookup from "./lookup" export default function renderTemplate(tokens, data){ let resultStr = '' for(let i = 0,length = tokens.length; i<length; i++){ let token = tokens[i] if(token[0] == 'text'){ resultStr += token[1] } else if(token[0] == 'name'){ resultStr += lookup(data,token[1]) }else if(token[0] == '#'){ } } return resultStr }
|
在主程序main.js中引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
import parseTemplateToTokens from './parseTemplateToTokens.js' import renderTemplate from './renderTemplate.js'
window.MyTemplate = { render(templateStr, data){ let tokens = parseTemplateToTokens(templateStr) let domStr = renderTemplate(tokens, data) console.log(domStr) } }
|
完成递归内容
本部分对lookup进行修改,对parseArray进行了定义,对renderTemplate进行了补充,后边两个互相引入,递归调用,多理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
export default function lookup(dataObj, keyName){ if (keyName.indexOf('.') != -1 && keyName != '.'){ let temp = dataObj let names = keyName.split('.'); for(let i=0; i<names.length; i++){ temp = temp[names[i]] } return temp } return dataObj[keyName]
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
import lookup from "./lookup.js" import parseArray from './parseArray.js' export default function renderTemplate(tokens, data){ let resultStr = '' for(let i = 0,length = tokens.length; i<length; i++){ let token = tokens[i] if(token[0] == 'text'){ resultStr += token[1] } else if(token[0] == 'name'){ resultStr += lookup(data,token[1]) }else if(token[0] == '#'){ resultStr += parseArray(token, data) } } return resultStr }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
import lookup from "./lookup"; import renderTemplate from "./renderTemplate";
export default function parseArray(token, data){ let v = lookup(data, token[1]) let resultStr = '' for(let i=0; i<v.length; i++){ resultStr += renderTemplate(token[2],{ ...v[i], '.': v[i], }) } return resultStr
}
|
在主程序main.js中返回最后的DOM字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
import parseTemplateToTokens from './parseTemplateToTokens.js' import renderTemplate from './renderTemplate.js'
window.MyTemplate = { render(templateStr, data){ let tokens = parseTemplateToTokens(templateStr) let domStr = renderTemplate(tokens, data) return domStr; } }
|
现在我们就可以在html中使用更复杂的模板,而且可以挂载到真实DOM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <body> <div id="container">
</div> <script src="/xuni/bundle.js"></script> <script> let templateStr = ` <div> <ol> {{#students}} <li> 学生{{name}}的爱好是 <ol> {{#hobbies}} <li>{{.}}</li> {{/hobbies}} </ol> </li> {{/students}} </ol> </div> ` let data = { students: [ { 'name': '小明', 'hobbies': ['游泳', '健身'] }, { 'name': '小红', 'hobbies': ['足球', '篮球', '羽毛球'] }, { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] } ] } let realDomStr = MyTemplate.render(templateStr, data) document.getElementById('container').innerHTML = realDomStr </script> </body>
|