vue源码之mustache模板引擎
vue源码之mustache模板引擎
什么是模板引擎
模板引擎是将数据变为视图
的解决方案;
1、纯DOM法;
2、数据join法(字符串);
3、es6的反引号法;
4、模板引擎;
<body>
<ul id="list"></ul>
<script>
var str = ['a', 'b', 'c', 'd'].join('')
var str = [
'a',
'b',
'c',
'd'
].join('')
console.log(str); // abcd
var html = [
'<ul>',
'<li>姓名:</li>',
'<li>年龄:</li>',
'<li>性别:</li>',
'</ul>'
].join('')
console.log(html);
var arr = [
{ "name": "fqniu", age: 25, sex: 'boy' },
{ "name": "niuniu", age: 18, sex: 'boy' },
{ "name": "niuer", age: 24, sex: 'boy' },
]
var list = document.getElementById('list')
// 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
for(let i=0;i<arr.length;i++){
list.innerHTML += [
'<ul>',
'<li>姓名:'+arr[i].name+'</li>',
'<li>年龄:'+arr[i].age+'</li>',
'<li>性别:'+arr[i].sex+'</li>',
'</ul>'
].join('')
}
// 反引号写法
for(let i=0;i<arr.length;i++){
list.innerHTML += `
<ul>
<li>姓名:${arr[i].name}</li>
<li>年龄:${arr[i].age}</li>
<li>性别:${arr[i].sex}</li>
</ul>
`
}
</script>
</body>
mustache 基本使用
mustache是最早的模板引擎库;引入mustache库,可以在bootcdn.com上找到;
mustache的模板语法比较简单,如下:
<div id="wrapper"></div>
// 引入 mustache 文件
<script src="./mustache.js"></script>
<script>
console.log(Mustache);
// # 代表循环开始 /代表循环结束
var templateStr = `
<ul>
{{#arr}}
<li>
<div>姓名:{{name}}</div>
<div>年龄:{{age}}</div>
<div>性别:{{sex}}</div>
</li>
{{/arr}}
</ul>
`;
var data ={
arr:[
{ "name": "fqniu", age: 25, sex: 'boy' },
{ "name": "niuniu", age: 18, sex: 'boy' },
{ "name": "niuer", age: 24, sex: 'boy' },
]
};
var domStr = Mustache.render(templateStr,data)
console.log(domStr);
var wrapper = document.getElementById('wrapper')
wrapper.innerHTML = domStr
</script>
mustache 底层核心机理
// 简单的模板引擎实现机理。利用的是正则表达式中的replace方法
// replace() 的第二个参数可以是一个函数,函数提供捕获的东西参数,就是captureStr,最后结合data对象,进行智能的替换
function render(templateStr, data) {
return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, captureStr) {
return data[captureStr]
})
}
tokens是一个js的嵌套数组,就是模板字符串的JS表示;
它是 “抽象语法书” 、“虚拟节点”;
手写实现mustache库
mustache.js中的scan
mustache库之scanner
/**
* 扫描器类
*
*/
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) {
// tag有多长,比如{{长度为2,就让指针后移多少位
this.pos += tag.length;
// 尾巴也要变, 改变尾巴,从当前指针这个字符开始,到最后全部字符
this.tail = this.templateStr.substring(this.pos)
}
}
// 让指针进行扫描,直到遇见内容结束,并且能够返回结束之前路过的文字
scanUntil(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)
}
// eos 指针是否已经到头,返回布尔值 end of string
eos(){
return this.pos >= this.templateStr.length
}
}
index.js
import Scanner from './Scanner'
window.mt_templateEngine = {
render(templateStr,data){
console.log('render函数被调用了');
// 实例化一个扫描器,构造时候提供一个参数,这个参数就是模板字符串
// 可以说扫描器就是针对这个字符串工作的
var scanner = new Scanner(templateStr)
var word;
// 当 scanner 没有到头
// while(scanner.pos !== templateStr.length){
while(!scanner.eos()){
word = scanner.scanUntil("{{")
console.log(word);
scanner.scan("{{")
word = scanner.scanUntil("}}")
console.log(word);
scanner.scan("}}")
}
}
}
mustache库之token
import Scanner from './Scanner'
/**
* 将模板字符串变为tokens数组
*/
export default function parseTemplateToToken(templateStr) {
var tokens = []
// 创建扫描器
var scanner = new Scanner(templateStr)
var words
// 扫描器工作
while(!scanner.eos()){
// 收集开始标记出现之前的文字
words = scanner.scanUntil('{{')
// 判断是否为空的情况
if(words !==''){
// 这个words就是{{}}中间的东西,判断一下首字符
if(words[0] == '#'){
// 存起来,从下标为1的开始存,因为下标为0的是 #
tokens.push(['#',words.substring(1)])
} else if (words[0] == '/'){
// 存起来,从下标为1的开始存,因为下标为0的是 /
tokens.push(['/',words.substring(1)])
} else {
// 存起来
tokens.push(['text',words])
}
}
// // 判断是否为空的情况
// if(words !==''){
// // 存起来
// tokens.push(['name',words])
// }
// 过双大括号
scanner.scan('{{')
// 收集开始标记出现之前的文字
words = scanner.scanUntil('}}')
// 判断是否为空的情况
if(words !==''){
// 这个words就是{{}}中间的东西,判断一下首字符
if(words[0] == '#'){
// 存起来,从下标为1的开始存,因为下标为0的是 #
tokens.push(['#',words.substring(1)])
} else if (words[0] == '/'){
// 存起来,从下标为1的开始存,因为下标为0的是 /
tokens.push(['/',words.substring(1)])
} else {
// 存起来
tokens.push(['text',words])
}
}
// 过双大括号
scanner.scan('}}')
}
return tokens
}
将零散的tokens嵌套起来
这里的结构为 栈 (first in last out):FILO 先进后出;
遇见 # 就进栈,遇见 / 就出栈
根据源码引出如下
/**
* nestTokens 函数是用于折叠tokens
* 将 # 和 / 之间的tokens整合起来,作为下标为3的项
*
*/
export default function nestTokens(tokens) {
// 结果数组
var nestedTokens = [];
// 栈结构 存放小tokens ,栈顶(靠近端口的,最新进入的) 的tokens数组中当前操作的这个tokens小数组
var sections = []
// console.log(tokens);
// 收集器 天生指向 nestedTokens结果数组,引用类型值,指向同一个数组
// 注意 收集器指向会变化 当遇见 # 收集器会指向这个token下标为2的新数组
var collector = nestedTokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case '#':
// // 给这个tokens下标为2的项创建一个数组,以收集子元素
// token[2] = []
// // 压栈 (入栈)
// sections.push(token)
// // console.log(token[1],'进栈');
// nestedTokens.push(token)
// 收集器中放入token
collector.push(token)
// (入栈)
sections.push(token)
// 收集器换人, 给这个token添加下标为2的项,并且让收集器指向他
collector = token[2] = []
break;
case '/':
// // 弹栈 出栈 pop() 会返回弹出的项
// let section_pop = sections.pop()
// // console.log(section[1],'出栈');
// // 刚刚弹出的项还没有加入到结果数组中
// nestedTokens.push(section_pop)
// 出栈 pop() 会返回刚刚弹出的项
let section_pop = sections.pop()
// 改变收集器为栈结构队尾(队尾是栈顶) 那项下标为2的数组
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens
break;
default:
// // 判断。栈队列当前情况
// if(sections.length == 0){
// nestedTokens.push(token)
// }else{
// sections[sections.length -1][2].push(token)
// }
collector.push(token)
}
}
return nestedTokens;
}
编写renderTemplate 函数让tokens 变为dom字符串
先写一种较为简单的形式但是不全面的函数
/**
* 函数的功能是让tokens 数组变为dom字符串
*
*/
export default function renderTempla(tokens, data) {
console.log(tokens, data);
// 结果字符串
var resultStr = ''
// 循环tokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// 看类型
if (token[0] == 'text') {
// 拼起来
resultStr += token[1]
} else if (token[0] == 'name') {
resultStr += data[token[1]]
}
}
console.log(resultStr);
}
index.js
import Scanner from './Scanner';
import parseTemplateToToken from './parseTemplateToToken';
import renderTemplate from './renderTemplate'
window.mt_templateEngine = {
render(templateStr, data){
// 调用 parseTemplateToToken 让模板字符串变为tokens数组
var tokens = parseTemplateToToken(templateStr)
// 调用renderTemplate 函数,让token数组变为dom字符串
var domStr = renderTemplate(tokens, data)
console.log(tokens);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>我是index.html</div>
<script src="/xuni/bundle.js"></script>
<script>
var templateStr = '我爱{{things}},{{things}}也好吃!';
var data = {
things:'做饭'
}
mt_templateEngine.render(templateStr,data)
</script>
</body>
</html>
注意: # 标记的tokens 需要递归处理 它的下标为2的数组
遇见问题:不认识 . 符号
例如 a.b.c
lookup.js 在dataObj对象中,寻找用连续点符号的keyname属性
/**
*
* 功能是可以在dataObj对象中,寻找用连续点符号的keyname属性
* 比如 obj.a.b.c
* {
* a: {
* b: {
* c:100
* }
* }
* }
*
* 那么lookup (dataObj, 'a.b.c') 结果是100
*
*/
export default function lookup(dataObj, keyname) {
console.log(dataObj, keyname);
// 看看keyname 有没有点符号 但是不能是 . 本身
if (keyname.indexOf('.') !== -1 && keyname !== '.') {
var keys = keyname.split('.')
// 用这个temp变量作为中间值 临时变量 用于周转 , 一层一层去找
var temp = dataObj;
// 每找一层设置为新的临时变量
for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]];
}
return temp
}
// 如果这里没有点符号
return dataObj[keyname];
}
parseArray.js 处理数组,结合renderTemplate实现递归
/**
*
* 处理数组,结合renderTemplate实现递归
* 注意 函数接收的是 token 而不是tokens
* token 就是简单的 ['#', 'xxx',[]]
* 这个函数要递归调用renderTemplate 函数 调用多少次 取决于 data 决定
* {
arr:[
{name:'fqniu',age:'25',sex:'boy',hobbise:['游泳','健身']},
{name:'fqniu',age:'25',sex:'boy',hobbise:['游泳','健身']},
]
}
那么parseArray 函数就要调用renderTemplate 函数 2次 因为data数组长度为 2
*
*/
import lookup from './lookup'
import renderTemplate from './renderTemplate';
export default function parseArray(token, data) {
// console.log(token, data);
// 得到整体数据中这个数组要使用的部分
var v = lookup(data, token[1])
// console.log('v=',v);
var resultStr = ''
// 遍历v数组,v 一定是数组
// 注意这个循环 是最难想到的 是遍历数据,不是遍历tokens, 数组中的数据有几个,就是遍历几条
for (let i = 0; i < v.length; i++) {
// 这里要补充一个 . 的 属性 先添加一个. 属性 然后再展开
resultStr += renderTemplate(token[2], {
...v[i],
'.':v[i],
})
}
return resultStr
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>我是index.html</div>
<script src="/xuni/bundle.js"></script>
<script>
// var templateStr = `<h1>我今天很{{mood}},明天也一样{{mood}}</h1>`
var templateStr = `
<div>
<ol>
{{#arr}}
<li>
<div>姓名:{{name}}</div>
<div>年龄:{{age}}</div>
<div>性别:{{sex}}</div>
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/arr}}
</ol>
<div>
`
var data = {
arr:[
{name:'fqniu',age:'25',sex:'boy',hobbies:['游泳','健身']},
{name:'niuniu',age:'19',sex:'boy1',hobbies:['游泳1','健身1']},
]
}
var domStr = mt_templateEngine.render(templateStr,data)
console.log(domStr);
</script>
</body>
</html>
总结如下代码内容
/**
* 扫描器类
*
*/
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) {
// tag有多长,比如{{长度为2,就让指针后移多少位
this.pos += tag.length;
// 尾巴也要变, 改变尾巴,从当前指针这个字符开始,到最后全部字符
this.tail = this.templateStr.substring(this.pos)
}
}
// 让指针进行扫描,直到遇见内容结束,并且能够返回结束之前路过的文字
scanUntil(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)
}
// eos 指针是否已经到头,返回布尔值 end of string
eos(){
return this.pos >= this.templateStr.length
}
}
/**
* nestTokens 函数是用于折叠tokens
* 将 # 和 / 之间的tokens整合起来,作为下标为3的项
*
*/
export default function nestTokens(tokens) {
// 结果数组
var nestedTokens = [];
// 栈结构 存放小tokens ,栈顶(靠近端口的,最新进入的) 的tokens数组中当前操作的这个tokens小数组
var sections = []
// console.log(tokens);
// 收集器 天生指向 nestedTokens结果数组,引用类型值,指向同一个数组
// 注意 收集器指向会变化 当遇见 # 收集器会指向这个token下标为2的新数组
var collector = nestedTokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i];
switch (token[0]) {
case '#':
// // 给这个tokens下标为2的项创建一个数组,以收集子元素
// token[2] = []
// // 压栈 (入栈)
// sections.push(token)
// // console.log(token[1],'进栈');
// nestedTokens.push(token)
// 收集器中放入token
collector.push(token)
// (入栈)
sections.push(token)
// 收集器换人, 给这个token添加下标为2的项,并且让收集器指向他
collector = token[2] = []
break;
case '/':
// // 弹栈 出栈 pop() 会返回弹出的项
// let section_pop = sections.pop()
// // console.log(section[1],'出栈');
// // 刚刚弹出的项还没有加入到结果数组中
// nestedTokens.push(section_pop)
// 出栈 pop() 会返回刚刚弹出的项
sections.pop()
// 改变收集器为栈结构队尾(队尾是栈顶) 那项下标为2的数组
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens
break;
default:
// // 判断。栈队列当前情况
// if(sections.length == 0){
// nestedTokens.push(token)
// }else{
// sections[sections.length -1][2].push(token)
// }
collector.push(token)
}
}
return nestedTokens;
}
import Scanner from './Scanner'
import nestTokens from './nestTokens'
/**
* 将模板字符串变为tokens数组
*/
export default function parseTemplateToToken(templateStr) {
var tokens = []
// 创建扫描器
var scanner = new Scanner(templateStr)
var words
// 扫描器工作
while (!scanner.eos()) {
// 收集开始标记出现之前的文字
words = scanner.scanUntil('{{')
// 判断是否为空的情况
if (words !== '') {
// 尝试写一下去掉空格,智能判断是普通文字的空格,还是标签中的空格
// 标签中的空格不能去掉,比如<div class="xxx"></div>中的空格不能去掉
// 是否是尖角号
let isInjjh = false;
// 空白字符串
let _words = '';
for (let i = 0; i < words.length; i++) {
// 判断是否在标签内
if (words[i] == '<') {
isInjjh = true;
} else if (words[i] == '>') {
isInjjh = false;
}
// 如果不是空格,拼接上
if (!/\s/.test(words[i])) {
_words += words[i];
} else {
// 如果是空格,只有当他的标签内的时候,才拼接上
if (isInjjh) {
_words += ' ';
}
}
}
// 存起来 去掉空格
tokens.push(['text', _words])
}
// 过双大括号
scanner.scan('{{')
// 收集开始标记出现之前的文字
words = scanner.scanUntil('}}')
// 判断是否为空的情况
if (words !== '') {
// 这个words就是{{}}中间的东西,判断一下首字符
if (words[0] == '#') {
// 存起来,从下标为1的开始存,因为下标为0的是 #
tokens.push(['#', words.substring(1)])
} else if (words[0] == '/') {
// 存起来,从下标为1的开始存,因为下标为0的是 /
tokens.push(['/', words.substring(1)])
} else {
// 存起来
tokens.push(['name', words])
}
}
// 过双大括号
scanner.scan('}}')
}
// 返回折叠后的tokens
return nestTokens(tokens)
}
/**
*
* 处理数组,结合renderTemplate实现递归
* 注意 函数接收的是 token 而不是tokens
* token 就是简单的 ['#', 'xxx',[]]
* 这个函数要递归调用renderTemplate 函数 调用多少次 取决于 data 决定
* {
arr:[
{name:'fqniu',age:'25',sex:'boy',hobbise:['游泳','健身']},
{name:'fqniu',age:'25',sex:'boy',hobbise:['游泳','健身']},
]
}
那么parseArray 函数就要调用renderTemplate 函数 2次 因为data数组长度为 2
*
*/
import lookup from './lookup'
import renderTemplate from './renderTemplate';
export default function parseArray(token, data) {
// console.log(token, data);
// 得到整体数据中这个数组要使用的部分
var v = lookup(data, token[1])
// console.log('v=',v);
var resultStr = ''
// 遍历v数组,v 一定是数组
// 注意这个循环 是最难想到的 是遍历数据,不是遍历tokens, 数组中的数据有几个,就是遍历几条
for (let i = 0; i < v.length; i++) {
// 这里要补充一个 . 的 属性 先添加一个. 属性 然后再展开
resultStr += renderTemplate(token[2], {
...v[i],
'.':v[i],
})
}
return resultStr
}
/**
*
* 功能是可以在dataObj对象中,寻找用连续点符号的keyname属性
* 比如 obj.a.b.c
* {
* a: {
* b: {
* c:100
* }
* }
* }
*
* 那么lookup (dataObj, 'a.b.c') 结果是100
*
*/
export default function lookup(dataObj, keyname) {
// console.log(dataObj, keyname);
// 看看keyname 有没有点符号 但是不能是 . 本身
if (keyname.indexOf('.') !== -1 && keyname !== '.') {
var keys = keyname.split('.')
// 用这个temp变量作为中间值 临时变量 用于周转 , 一层一层去找
var temp = dataObj;
// 每找一层设置为新的临时变量
for (let i = 0; i < keys.length; i++) {
temp = temp[keys[i]];
}
return temp
}
// 如果这里没有点符号
return dataObj[keyname];
}
/**
* 函数的功能是让tokens 数组变为dom字符串
*
*/
import lookup from './lookup'
import parseArray from './parseArray'
export default function renderTemplate(tokens, data) {
// console.log(tokens, data);
// 结果字符串
var resultStr = ''
// 循环tokens
for (let i = 0; i < tokens.length; i++) {
let token = tokens[i]
// 看类型
if (token[0] == 'text') {
// 拼起来
resultStr += token[1]
} else if (token[0] == 'name') {
// 如果是name 类型 直接使用它的值 当然要用lookup函数 防止a.b.c形式取值为 undefined
resultStr += lookup(data, token[1])
}else if (token[0] == '#') {
// # 标记的tokens 需要递归处理 它的下标为2的数组
resultStr += parseArray(token, data)
}
}
return resultStr;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>我是index.html</div>
<script src="/xuni/bundle.js"></script>
<script>
var templateStr = `
<div>
<ol>
{{#arr}}
<li class="item">
<div>姓名:{{name}}</div>
<div>年龄:{{age}}</div>
<div>性别:{{sex}}</div>
<ol>
{{#hobbies}}
<li>{{.}}</li>
{{/hobbies}}
</ol>
</li>
{{/arr}}
</ol>
<div>
`
var data = {
arr:[
{name:'fqniu',age:'25',sex:'boy',hobbies:['游泳','健身']},
{name:'niuniu',age:'19',sex:'boy1',hobbies:['游泳1','健身1']},
]
}
var domStr = mt_templateEngine.render(templateStr,data)
console.log(domStr);
</script>
</body>
</html>
具体的可百度 mustache相关认识点 或 查看mustache 源码写法mustache.js