mustache 实现
mustache 是什么?
初次接触 mustache 是在 vue 中,当时还比较奇怪为什么叫 mustache。之后在探究 vue mustache 的实现原理中,才了解到是有一个叫 mustache 的库。
mustache即模版引擎,是将数据变为视图解决方案。
<ul>
<li v-for="user in users" >
姓名:{{ user.name }}
年龄:{{ user.age }}
</li>
</ul>
data: {
users: [
{
id: 1,
name: 'tom',
age: 20,
},
{
id: 2,
name: 'jerry',
age: 20,
}
]
},
在 vue 中通过模版引擎,我们可以非常优雅的将数据映射到视图中。
一切技术都是在迭代中发展出来的,所以要了解 mustache 的实现原理之前,我们还需要探究数据映射到视图中的演变史。
数据映射到视图的演变史
DOM 法
<ul id="list">
</ul>
<script>
const users = [
{
id: 1,
name: 'tom',
age: 20,
},
{
id: 2,
name: 'jerry',
age: 20,
}
];
let listEl = document.getElementById("list");
for (let i = 0; i < users.length; i++) {
// 创建 li 元素
const li = document.createElement("li");
li.innerText = '姓名:' + users[i].name + '年龄:' + users[i].age;
// 上树
list.appendChild(li);
}
</script>
数组 join 法
const users = [
{
id: 1,
name: 'tom',
age: 20,
},
{
id: 2,
name: 'jerry',
age: 20,
}
];
let listEl = document.getElementById("list");
for (let i = 0; i < users.length; i++) {
const str = [
'<li>' +
'姓名:' + users[i].name +
'年龄:' + users[i].age +
'</li>'
].join('');
listEl.innerHTML += str;
}
ES6 反引号法
const users = [
{
id: 1,
name: 'tom',
age: 20,
},
{
id: 2,
name: 'jerry',
age: 20,
}
];
let listEl = document.getElementById("list");
for (let i = 0; i < users.length; i++) {
const str = `
<li>
姓名:${users[i].name}
年龄:${users[i].age}
</li>`
listEl.innerHTML += str;
}
mustache
<div id="container">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
var templateStr = `
<ul>
{{ #users }}
<li>
姓名:{{ name }}
年龄:{{ age }}
</li>
{{/users}}
</ul>
`;
const data = {
users: [
{
id: 1,
name: 'tom',
age: 20,
},
{
id: 2,
name: 'jerry',
age: 20,
}
]
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
</script>
在上面的例子中,由于 DOM 的嵌套结构不够深,所以看起来可能实现方案都可以。但是如果 DOM 树的嵌套层次比较深呢?这样的话就可以明显感受出不同。
除了 DOM 法,其余的方法都是采用将 DOM 树转为字符串,然后利用 innterHTML
实现上树。并且 mustache 自动将数据注入到模版中。
mustache 基本使用
在上面的代码中,我们已经引用到了 mustache,并且做了简单的使用,接下来了解一下其提供的其他语法。
数据填充
<script>
var templateStr = `
<h1>{{ name }} 的年龄是{{ age }}</h1>`;
var data = {
name: 'tom',
age: 20,
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
</script>
循环简单数组
var templateStr = `
<ul>
{{ #arr }}
<li>
{{ . }}
</li>
{{/arr}}
</ul>
`;
var data = {
arr: ['a', 'b', 'c']
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
循环对象数组
var templateStr = `
<ul>
{{ #arr }}
<li>
<div class="hd">{{ name }}的基本信息</div>
<div class="bd">
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
</div>
</li>
{{/arr}}
</ul>
`;
var data = {
arr: [
{ "name": "小明", "age": 12, "sex": "男" },
{ "name": "小红", "age": 11, "sex": "女" },
{ "name": "小白", "age": 13, "sex": "男" },
]
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
循环嵌套数组
var templateStr = `
<ul>
{{ #arr }}
<li>
<div class="hd">{{ name }}的基本信息</div>
<div class="bd">
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>爱好:
<ol>
{{ #hobbies}}
<li>{{.}}</li>
{{ /hobbies }}
</ol>
</p>
</div>
</li>
{{/arr}}
</ul>
`;
var data = {
arr: [
{ "name": "小明", "age": 12, "hobbies": ["游泳", "羽毛球"] },
{ "name": "小红", "age": 11, "hobbies": ["编程", "篮球"] },
{ "name": "小白", "age": 13, "hobbies": ["游戏"] },
]
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
</script>
条件判断
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.0.1/mustache.js"></script>
<script>
var templateStr = `
{{ #res }}
<h1>hello world</h1>
{{ /res }}
`;
var data = {
res: false,
};
var domStr = Mustache.render(templateStr, data);
const container = document.getElementById('container');
container.innerHTML = domStr;
</script>
mustache 核心原理
1、将模版字符串编译为 tokens 形式。
2、将 tokens 结合数据,解析为 dom 字符串。
tokens 是什么?
tokens 为模版字符串的 JS 对象的表示,其是一个嵌套的 JS 数组。虚拟节点、抽象语法树的实现都是借鉴了 tokens 的思想。
<h1>{{ name }} 的年龄是{{ age }}</h1>`;
对应的 tokens 为
[
['text', '<h1>'],
['name', 'name'],
['text', ' 的年龄是'],
['name', 'age'],
['text', '</h1>']
]
<div>
<ul>
{{#arr}}
<1i>{{.}}</1i>
{{/arr}}
</ul>
</div>
对应的 tokens 为
[
["text", "div><ul>"],
["#", "arr", [
["text", "<li>"],
["name", "."],
["text", "</li>"]
]
],
["text", "</ul></div>"]
]
手写 mustache
生成 tokens 数组
分割模版字符串
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) {
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);
}
// 指针是否已经到头,返回 布尔值
eos() {
return this.pos >= this.templateStr.length;
}
}
生成 tokens
import Scanner from './Scanner.js';
/**
* 将模版字符串变为 tokens 数组
* @param {*} templateStr
*/
export default function parseTemplateToTokens(templateStr) {
let tokens = [];
var scanner = new Scanner(templateStr);
var words;
while (!scanner.eos()) {
// 收集开始标记出现之前对文字
words = scanner.scanUtil("{{");
if (words != '') {
tokens.push(['text', words]);
}
scanner.scan("{{");
// 收集双大括号里面的内容
words = scanner.scanUtil("}}");
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;
}
折叠数组
import Scanner from './Scanner.js';
/**
* 折叠tokens,将 # 和 / 之间的 tokens 折叠起来,形成嵌套数组
* @param {*} templateStr
*/
export default function nestTokens(tokens) {
var nestedTokens = [];
var collector = nestedTokens;
var sections = [];
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
case '#':
case '^':
collector.push(token);
sections.push(token);
collector = token[2] = [];
break;
case '/':
section = sections.pop();
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
collector.push(token);
}
}
return nestedTokens;
}
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';
/**
* 将模版字符串变为 tokens 数组
* @param {*} templateStr
*/
export default function parseTemplateToTokens(templateStr) {
let tokens = [];
var scanner = new Scanner(templateStr);
var words;
while (!scanner.eos()) {
// 收集开始标记出现之前对文字
words = scanner.scanUtil("{{");
if (words != '') {
tokens.push(['text', words]);
}
scanner.scan("{{");
// 收集双大括号里面的内容
words = scanner.scanUtil("}}");
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);
}
tokens 注入数据
解析模版中属性
export default function lookup(dataObj, keyName) {
if (keyName === '.') {
return dataObj;
}
let keyArr = keyName.split('.');
let obj = dataObj;
for (let key of keyArr) {
obj = obj[key];
}
return obj;
}
注入数据
import Scanner from './Scanner.js';
import nestTokens from './nestTokens.js';
import lookup from './lookup.js';
import { render } from 'vue';
/**
* 将 tokens 数组渲染数据后,转为 DOM 字符串
*/
export default function renderTemplate(tokens, data) {
var domStr = '';
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case 'text':
domStr += token[1];
break;
case 'name':
domStr += lookup(data, token[1]);
break;
case '#':
const tokenData = lookup(data, token[1]);
for (let j = 0; j < tokenData.length; j++) {
domStr += renderTemplate(token[2], tokenData[j]);
}
break;
default:
break;
}
}
return domStr;
}
转载自:https://juejin.cn/post/7134180801969111070