首先什么是模板引擎呢?
模板引擎是将数据要变为视图最优雅的解决方案.
中文名为胡子,也就是我们在vue中使用的{{}};
mustache通过render函数解析 由模板字符串转化为的token与数据data结合生成dom模板。如下图所示:
实现原理分析:
1. 将模板字符串编译成tokens,其中tokens是js的二维数组
2. 数据与 tokens 结合解析为 dom 字符串。
mustache 的底层核心原理
- mustache 库不能用简单的正则表达式思路实现
- 在较为简单的示例情况下,可以用正则表达式实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
var templateStr = '<h1>我买了一个{{thing}},好{{mood}}</h1>';
var data = {
thing: '白菜',
money: 5,
mood: '激动'
};
// 最简单的模板引擎的实现机理,利用的是正则表达式中的replace()方法。
// replace()的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,
// 第一个参数findStr就找到的匹配内容 第三个参数就是原串
// 正则()内的内容可以进行捕获 第二个参数$1就是捕获到的东西
// 结合data对象,即可进行智能的替换
function render(templateStr, data) {
return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {
return data[$1];
});
}
var result = render(templateStr, data);
console.log(result);
</script>
</body>
</html>
手写实现代码:
第一步:将模板字符串转换为Tokens
scanner.js
用于扫描模板字符串,找到tag内的变量,与tag外的变量
/* 扫描器类*/
export default class Scanner {
constructor(templateStr) {
this.templateStr = templateStr;// 将模板字符串写到实例身上
this.pos = 0; // 指针
this.tail = templateStr;// 尾巴,起始为模板字符串原文,后边指向未扫描部分的内容
}
// 功能弱,就是走过指定内容,没有返回值(作用:跳过 {{ }} )
scan(tag) {
if (this.tail.indexOf(tag) == 0) {
// tag有多长,比如{{长度是2,就让指针后移多少位
this.pos += tag.length;
// 尾巴也要变,改变尾巴为从当前指针这个字符开始,到最后的全部字符
this.tail = this.templateStr.substring(this.pos);
}
}
// 让指针进行扫描,直到遇见指定内容结束,并且能够返回结束之前路过的文字
// 作用:返回遇到 {{ }} 之前的内容
scanUtil(stopTag) {
// 记录从哪里开始扫描,既执行本方法时的pos值
const pos_backup = this.pos;
// 当尾巴的开头不是stopTag的时候,就说明还没有扫描到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);
}
// 指针是否已经到头,返回布尔值。end of string
eos() {
return this.pos >= this.templateStr.length;
}
};
parseTemplateToTokens.js
- 循环使用扫描器,直到将模板字符串全部扫描结束,最后将模板字符串变为tokens数组
import nestTokens from './nestTokens.js';
import Scanner from "./Scanner";
export default function parseTemplateToTokens(templateStr){
var tokens=[]; //存储内容
// 创建扫描器
var scanner = new Scanner(templateStr);
var words;
// 扫描器工作,当scanner没有到头时运行
while (!scanner.eos()) {
words = scanner.scanUtil("{{"); // 收集开始标记之前文字
if(words !=""){
// 去掉空格,智能判断是普通文字的空格,还是标签中的空格
// 标签中的空格不能去掉,比如<div class="box">不能去掉class前面的空格
let isJJH = false; //是否在尖角号<> 里面 ,默认不在
var _words = ""; // 空白字符串
for (let i = 0; i < words.length; i++) {
// 判断是否在标签中
if (words[i] == "<") {
isJJH = true;
} else if (words[i] == ">") {
isJJH = false;
}
// 如果这项不是空格,拼接上 \s 空格 \S非空格
if(!/\s/.test(words[i])){
_words += words[i];
}else{
//是空格,只有在当它在标签内时,才拼接上
if(isJJH){
_words += words[i];
}
}
}
// console.log(words);
// console.log(_words);
tokens.push(["text",_words]);//把去掉空格的文字存起来
}
scanner.scan("{{"); //过双大括号
words = scanner.scanUtil("}}"); // 收集属性名
if (words != "") {
// 这个words就是{{}}中间的东西。判断一下首字符
if(words[0] == "#"){ //循环属性 嵌套标记
// 存起来,从下标为1的项开始存,因为下标为0的项是#
tokens.push(["#",words.substring(1)]);
}else if(words[0]=="/"){ //循环属性 循环结束标记
tokens.push(["/", words.substring(1)]);
}else{//正常属性
tokens.push(["name",words]);//存起来
}
}
scanner.scan("}}"); //过双大括号
}
// 返回折叠的tokens
// console.log(nestTokens(tokens));
return nestTokens(tokens);
}
nestToken.js
- 整合折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
/*
函数的功能是折叠tokens,将#和/之间的tokens整合起来,作为它的下标为3的项.即从一维变为嵌套数组
*/
export default function nestTokens(tokens) {
var resultTokens = [];// 存放结果数组
// 栈结构,存放栈顶操作的小tokens;
// 栈顶,即tokens数组中当前操作的token小数组
var stack = []; //栈顶小tokens
// 收集器,天生指向resultTokens结果数组,引用类型值,所以指向的是同一个数组
// 收集器的指向会变化,当遇见#的时候,收集器会指向这个token的下标为2的新数组
var collector = resultTokens;
// console.log(tokens);
// 遍历每个tokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i]; //[0]:类型 [1]:值 [2]:子token
debugger;
// 对token的首项进行判断
switch (token[0]) {
// 如果token[0]是"#"说明是循环数组,需要使用栈思想进行嵌套
case "#":
//此时resultTokens和collector指向同一个引用地址(则resultTokens也会push值token)
collector.push(token);
stack.push(token); //token入账
//将收集器collector的指向变成当前token的第3项,将token用collector折叠
collector = token[2] = [];
break;
// 如果token[0]是"/",说明是循环结束(不需要推入数据)
case "/":
stack.pop(); // stack出栈
// 改变收集器为结构队尾(队尾才是栈顶,即数组下标为2处)
// 若stack长度大于2,说明还有循环,收集器collector指向上一个token的第三项
collector = stack.length > 0 ?
stack[stack.length - 1][2] : resultTokens;
break;
default:
// 甭管当前的collector是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,推入token即可。
collector.push(token);
}
}
// console.log(collector == resultTokens); //有时相等,有时不相等
return resultTokens;
}
/*
nestTokens()方法对传入的tokens数组进行遍历,普通项正常进入nestTokens数组,
遍历过程中若是遇到嵌套开始标志#,就将其压入Stack,并且后续对nestTokens的push操作都是在Stack栈顶数组中,
如果遇到嵌套结束标志/,可以判断当前Stack是否还有值,如果有则将出栈值push到栈顶数组中,
如果没有值了则将最后一个出栈的数组push到nestTokens中并将nestTokens返回。
该部分过于精妙:
难点:关于对象的引用
数组是对象,对象是引用赋值的同时对象的值是存在堆区的,如果两个变量保存的是同一个对象的引用,
一个变量改变时,另外一个也会跟着改变
所以数组中, 当collector变化时,会使得token[2]发生改变,进而让栈stack中的值token也发生改变
入栈后:stack存放入栈后的token,其中token中的token[2]是会随着for循环的不断继续而随collector不断变化的,
因而stack也会不断变化
出栈后:stack栈顶弹出,
resultTokens会在入栈后与collector值不相同,但是collector数组变化的推入的数据它也会跟着推入
遇到问题:无法准确表达出stack resultTokens collector三者之间的关系
影响:可能下次又无法理解了,下次看时 明白对象引用数据之间变化会相互影响就好
*/
第二步:数据结合tokens解析为Dom字符串
renderTemplate.js
- 让tokens数组变为dom字符串
import parseArray from './parseArray.js';
/*
函数的功能是让tokens数组变为dom字符串
*/
export default function renderTemplate(tokens, data) {
// console.log(tokens);
var resultStr = ""; //结果字符串
// 遍历tokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
// console.log(token);
// 看类型
if (token[0] == 'text') {
resultStr += token[1]; // 拼起来
} else if (token[0] == "name") {
// 如果是name类型,用lookup直接使用它的值
// lookup:识别“a.b.c”中.的形式
resultStr += lookup(data, token[1]);
} else if (token[0] == "#") {
resultStr += parseArray(token, data); //处理数组
}
}
return resultStr;
}
lookup.js
-
查找对象的某个属性
-
解决以下两个问题:
- obj[a.b.c],无法获取的问题
- 如果是遍历数组,mustache中是{{.}},就是直接返回这个obj
// dataObj数据对象,keyName属性名
export default function lookup(dataObj, keyName) {
// console.log(dataObj, keyName);
// 若keyName存在 . ,且不是.本身
if (keyName.indexOf(".") != -1 && keyName != ".") {
var keys = keyName.split("."); //使用.分离keyName
var temp = dataObj;//设置临时变量,寻找最终值
// console.log(keys);
for (let i = 0; i < keys.length; i++) {
// 每遍历一层,迭代更新一次变量(一层层剥)
temp = temp[keys[i]];
}
return temp;
}
// 若不存在 .
return dataObj[keyName];
}
parseArray.js
- 处理数组,结合renderTemplate实现递归
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';
export default function parseArray(token,data){
// console.log(token, data);
// 得到整体数据data中这个数组要使用的部分数据
var v = lookup(data,token[1]);
// console.log(v);
var resulutStr = "";
// 遍历v数组,v一定是数组(不做布尔值情况)
// 注意,下面这个循环可能是整个包中最难思考的一个循环!!!
// 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
for(let i =0;i<v.length;i++){
resulutStr += renderTemplate(token[2],{
...v[i], //处理 . 的情况
".":v[i]
});
}
return resulutStr;
}
第三步:向外暴露模板引擎与其render方法
index.js
- 整合以上方法,向外暴露模板引擎与其render方法
import parseTemplateToTokens from "./parseTemplateToTokens"
import renderTemplate from "./renderTemplate"
// 全局提供SGG_TempalteEngine对象
window.SGG_TempalteEngine = {
render(templateStr, data) {
// 调用parseTemplateToTokens函数,让模板字符串变成token数组
var tokens = parseTemplateToTokens(templateStr);
// 调用renderTemplate函数,让tokens数组变为dom字符串
var domStr = renderTemplate(tokens, data);
return domStr;
}
}