Vue 【进阶】- 模板引擎
vue的源码学习流程和知识点分析
本次您将学习到的东西
前期准备
1. 简介
1.1 什么是模板引擎
模板引擎是将数据要变为视图最优雅的解决方案
1.2 v-for 实际就是一种模板引擎
1.3 历史上曾出现的数据变为视图的方法
1.3.1 纯dom法 --- jq、js
1.3.2 数组 json 法
1.3.3 es6模板语法
2. mustache库简介
2.1 mustache库基本使用
umd 的包,浏览器和Npm node 环境都可以使用
2.1.1 下载 https://www.bootcdn.cn/
代码复制到 mustache.js
2.1.2 语法
- Mustache.render(tem,data) 循环对象数组
- Mustache.render(tem,data) 不循环
和 Vue 的模板用法一样 - 循环简单数组
- 数组可以嵌套
- 布尔值(类似vue v-if)
<script src="./mustache.js"></script>
<script>
var tmp = `
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}</div>
<div class="bd">
<p>{{name}}</p>
<p>{{sex}}</p>
<p>{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
`
var data = {
arr: [
{name:'123', age: 12, sex: 1},
{name:'123', age: 12, sex: 1},
{name:'123', age: 12, sex: 1},
]
}
var domStr = Mustache.render(tmp, data)
console.log(domStr)
</script>
2.2 template 的写法
原理 由于script标签只要type不等于 text/javascript 就不会解析,可以利用来写模板,就不用写在es6的模板语法里面了
<script type="text/template" id="tmp">
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}</div>
<div class="bd">
<p>{{name}}</p>
<p>{{sex}}</p>
<p>{{age}}</p>
</div>
</li>
{{/arr}}
</ul>
</script>
<script>
var tmp = document.getElementById('tmp').innerHTML
2.3 mustache库不能用简单的正则表达式思路实现
string.replace()第二个参数可以是个函数,这个函数的参数就是捕获的()里的$1
2.4 mustache库的机理
2.4.1 什么是 tokens?
2.4.2 编译原理
2.4.3 循环情况下
2.4.4 双循环情况下
3. 开始手写实现 mustache
了解完 tokens,现在开始自己写一个 My_TemplateEngine 对象来实现 Mustache 的功能。我们将把独立的功能拆写为独立的 js 文件,通常是一个独立的类,每个单独的功能都应能完成独立的单元测试。
实现将模板字符串编译为 tokens
3.1 实现 Scanner 类
Scanner 类的实例就是一个扫描器,用来扫描构造时作为参数提供的那个模板字符串。
- 属性
- pos:指针,用于记录当前扫描到字符串的位置
- tail:尾巴,值为当前指针之后的字符串(包括指针当前指向的那个字符)
- 方法
- scan:无返回值,让指针跳过传入的结束标识 stopTag
- scanUntil:传入一个指定内容 stopTag 作为让指针 pos 结束扫描的标识,并返回扫描内容
// Scanner.js
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr
// 指针
this.pos = 0
// 尾巴
this.tail = templateStr
}
scan(stopTag) {
this.pos += stopTag.length // 指针跳过 stopTag,比如 stopTag 是 {{,则 pos 就会加2
this.tail = this.templateStr.substring(this.pos) // substring 不传第二个参数直接截取到末尾
}
scanUntil(stopTag) {
const pos_backup = this.pos // 记录本次扫描开始时的指针位置
// 当指针还没扫到最后面,并且当尾巴开头不是 stopTag 时,继续移动指针进行扫描
// 注意 && 的必要性,可以避免死循环的发生
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) // 返回扫描过的字符串,不包括 this.pos 处
}
// 指针是否已经抵达字符串末端,返回布尔值 eos(end of string)
eos() {
return this.pos >= this.templateStr.length
}
}
3.2 根据模板字符串生成 tokens
有了 Scanner 类后,就可以着手去根据传入的模板字符串生成一个 tokens 数组了。最终想要生成的 tokens 里的每一条 token 数组的第一项用 name(数据) 或 text(非数据文本) 或 #(循环开始) 或 /(循环结束) 作为标识符。
新建一个 parseTemplateToTokens.js 文件来实现
// parseTemplateToTokens.js
import Scanner from './Scanner.js'
import nestTokens from './nestTokens' // 后面会解释
// 函数 parseTemplateToTokens
export default templateStr => {
const tokens = []
const scanner = new Scanner(templateStr)
let word
while (!scanner.eos()) {
word = scanner.scanUntil('{{')
word && tokens.push(['text', word]) // 保证 word 有值再往 tokens 里添加
scanner.scan('{{')
word = scanner.scanUntil('}}')
/**
* 判断从 {{ 和 }} 之间收集到的 word 的开头是不是特殊字符 # 或 /,
* 如果是则这个 token 的第一个元素相应的为 # 或 /, 否则为 name
*/
word && (word[0] === '#' ? tokens.push(['#', word.substr(1)]) :
word[0] === '/' ? tokens.push(['/', word]) : tokens.push(['name', word]))
scanner.scan('}}')
}
return nestTokens(tokens) // 返回折叠后的 tokens, 详见下文
}
3.3 在 index.js 引入 parseTemplateToTokens
// index.js
import parseTemplateToTokens from './parseTemplateToTokens.js'
window.My_TemplateEngine = {
render(templateStr, data) {
const tokens = parseTemplateToTokens(templateStr)
console.log(tokens)
}
}
这样我们就可以把传入的 templateStr 初步转成 tokens 了,比如 templateStr 为
const templateStr = `
<ul>
{{#arr}}
<li>
<div>{{name}}的基本信息</div>
<div>
<p>{{name}}</p>
<p>{{age}}</p>
<div>
<p>爱好:</p>
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</div>
</div>
</li>
{{/arr}}
</ul>
`
那么目前经过 parseTemplateToTokens 处理将得到如下的 tokens
接下去,就是要想办法,让循环的部分,也就是 # 和 / 之间的内容,作为以 # 为数组第 1 项元素的那个 token 的第 3 项元素。也就是把红框里的内容插入到 ["#","arr"] 内,成为其第 3 项元素;同理,将蓝框里的内容插入到 ["#","hobbies"] 内成为其第 3 项元素
3.4 实现 tokens 的嵌套
定义 handle 函数来做 tokens 的嵌套功能,将传入的 tokens 处理成包含嵌套的 handle 数组返回。
function handle(tokens) {
let arr = [],
stack = [], // 入栈
collector = arr // 收集器 - 共用一个指针
for(var i=0;i<tokens.length;i++) {
let token = tokens[i]
switch(token[0]) {
case '#':
//
collector.push(token)
// 这时 arr、stack、collector 的 token 都是共用一个指针的 token
stack.push(token)
// 这时 collector = token[2], collector改变,arr、stack 中 token[2] 随之改变
collector = token[2] = []
break;
case '/':
// 出栈, 返回刚刚弹出的项
stack.pop()
// 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
collector = stack.length > 0 ? stack[stack.length - 1][2] : arr
break;
default:
collector.push(token)
break;
}
}
return arr
}
我们已经成功实现了将模板字符串编译为 tokens 的过程,剩下的工作就是将 tokens 结合数据解析成 dom 字符串。即下图中的红框部分
3.5 tokens 结合数据解析为 dom 字符串
大致思路是遍历 tokens 数组,根据每条 token 的第一项的值来做不同的处理,为 text 就直接把 token[1] 加入到最终输出的 dom 字符串,为 name 则根据 token[1] 去 data 里获取数据,结合进来。
当 data 里存在多层嵌套的数据结构,比如 data = { test: { a: { b: 10 } } },这时如果某个 token 为 ["name", "test.a.b"],即代表数据的 token 的第 2 项元素是 test.a.b 这样的有多个点符号的值,那么我么直接通过 data[test.a.b] 是无法拿到正确的值的,因为 js 不认识这种写法。我们需要提前准备一个 lookup 函数,用以正确获取数据。
定义 lookup 函数
/**
* @description 可以在 dataObj 对象中,寻找用连续点符号的keyName属性 data.a.b data['a.b']
*/
export default function(data, key) {
let dataObj = data, keyArr = key.split('.')
if(key == '.') return data
if(keyArr.length > 1) {
while(keyArr.length > 0) {
dataObj = dataObj[keyArr[0]]
keyArr.shift()
}
} else {
return dataObj[key]
}
return dataObj
}
3.6 定义 renderTemplate 函数
接下来就可以开始写个 renderTemplate 函数将 tokens 和 data 作为参数传入,解析为 dom 字符串了。
需要注意的是遇到循环的情况,也就是当某个 token 的第一项为 "#" 时,要再次递归调用 renderTemplate 函数。这里我们新定义了一个 loop 函数来处理。
import lookup from "./lookup"
export default function renderTemplate(tokens, data) {
if(!tokens) return
let resStr = ''
tokens.forEach(item => {
switch(item[0]) {
case 'text':
resStr += item[1]
break;
case 'key':
resStr += lookup(data, item[1])
break;
case '#':
resStr += loop(data[item[1]],item[2])
case '.':
break;
default:
break;
}
})
// console.log(resStr)
return resStr
}
function loop(data, tokens) {
let resStr = ''
data.forEach(item => {
resStr += renderTemplate(tokens, item)
})
return resStr
}