源码地址:https://cdn.bootcdn.net/ajax/libs/mustache.js/1.0.0/mustache.js
大概的方法
{{name}} 会把数据中 name 转换为具体值
{{#list}} {{/list}} 会把list中的数据选项展示
{{^list}}{{/list}} 数据中 list不存在或者未空数组才展示
{{!注释}} 注释
{{{}}} 里面的内容不会转义
{{>name}} 会从附属数据中获取数据
<div>{{=<% %>=}}</div> 修改 <% %> 为新切割符号
html测试
<!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 id="app"></div>
<script src="./mustache1.0.js"></script>
<script>
var template = `
<div>{{name}}</div>
<div>{{age}}</div>
<div>{{!我是注释}}</div>
<div>{{{f1}}}</div>
<div>{{{f2}}}</div>
<div>{{<template2}}</div>
{{^list}}
<span>姓名:{{name}}<i>年龄:{{age}}</i></span>
{{/list}}
{{#arr}}
<span>{{test}}</span>
{{/arr}}
{{#arr2}}
<p>{{top}}</p>
{{/arr2}}
<div>{{>abc}}</div>
<div>{{=<% %>=}}</div>
<div><%name%></div>
`;
var template2 = '<sapn>{{sex}}</sapn';
var data = {
name: '张三',
age: 14,
f1: 'abc&<',
f2: function () {
return 666;
},
f3: 'abc&<',
sex: '男',
test: 'tttttttt',
list: [], //也可以不要这一项
arr: ()=> (template, fn)=>{
return fn(template)
},
arr2: [{top:'test1'},{top:'test2'}],
};
const str = Mustache.render(template, data, {abc: '123'});
// const str = Mustache.render(template, data, (a)=>{
// return a + '333'
// });
console.log('str', str);
</script>
</body>
</html>
原理
1.0是 大概原理是 把template模板 通过 对应的分隔符号 比如 {{ }} 转为为tokens;
["text", "</div>↵", 181, 188]
["^", "list", 194, 203, Array(5), 260]
["text", " <span>姓名:", 204, 221]
["name", "name", 221, 229]
["text", "<i>年龄:", 229, 235]
["name", "age", 235, 242]
["text", "</i></span>↵", 242, 254]
["/", "list", 260, 269]
["#", "arr", 276, 284, Array(3), 321]
[type, value, start, scanner.pos]
- 其中type 是这个 类型 text 为文本, name需要取数据中的键 ,# 是循环
- value 数据中的键或者字符串
- start 这个是当前数组所在字符串的位置
- scanner.pos 扫描器的位置或者未结束位置
然后把tokens处理为 dom识别的字符串;
源码里面有三个类:
- Scanner;
- Context;
- Writer;
Scanner 扫描器
我的理解 Scanner 是 当 匹配符号比如 {{ 和 }} 匹配一段文本是分别 获取 匹配符号之外的文本。(其实跟以前版本正则类似)
比例: 我是{{name}},我爱我的{{contry}}。也爱你
通过Scanner类可以获取到 五段内容:
- 我是
- name
- ,我爱我的
- contry
- 。也爱你
源码
/**
* 模板解析器用于在模板字符串中查找令牌的简单字符串扫描仪
* 获取字符串,并设置尾部,尾巴会向后减少。pos是当前位置
*/
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* 判断是否到尾部
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* 为了跳过匹配符号
* 返回匹配的匹配的符号,如果没有返回空字符串
* 比如 re为 {{ 找到的话就返回 {{ 并且尾巴向后移动两位,pos位置也加2
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
// 没有匹配直接返回空字符串
if (!match || match.index !== 0)
return '';
var string = match[0];
// 尾巴 向后移动 匹配符号的长度
this.tail = this.tail.substring(string.length);
// 位置加 上匹配符号的长度
this.pos += string.length;
return string;
};
/**
* 为了找到 匹配符号前的文本
* 如果有匹配的符号,返回匹配符号之前的文本,否则就是没有匹配到,返回剩下的尾巴
* 比如 这里是{{name}}的家 re为 {{ 会返回 这里是
*/
Scanner.prototype.scanUntil = function (re) {
// 得到匹配符号在尾巴中的位置(尾巴会变化,所有位置也会在变化)
var index = this.tail.search(re),
match;
switch (index) {
// -1说明没有匹配到特殊符号,说明已经扫描完了。直接返回剩下的文本。比把尾巴置空
case -1:
match = this.tail;
this.tail = "";
break;
// 特殊符号刚好在开始地方,那匹配符号前 就没有文本。匹配到的就是空字符串
case 0:
match = "";
break;
// 其他情况 直接获取匹配符号前的文本,并把尾巴向后移动到匹配的地方(这个时候后面会用scan函数跳过匹配符号)
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
// 位置 需要加上匹配的长度 ,并返回匹配的文本
this.pos += match.length;
return match;
};
html
<!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 id="app"></div>
<script src="./mustache1.0.js"></script>
<script>
var template = `
<div>{{name}}</div>
<div>{{age}}</div>
<div>{{!我是注释}}</div>
<div>{{{f1}}}</div>
<div>{{{f2}}}</div>
<div>{{f3}}</div>
<div>{{<template2}}</div>
{{#list}}
<span>姓名:{{name}}<i>年龄:{{age}}</i></span>
{{/list}}
`;
var scaner = new Mustache.Scanner(template)
console.log('scaner', scaner);
console.log('scanUntil', scaner.scanUntil("{{"));
console.log('scan', scaner.scan("{{"));
console.log('scanUntil', scaner.scanUntil("}}"));
console.log('scan', scaner.scan("}}"));
</script>
</body>
</html>
由于这个类向外暴露了。我们可以直接测试
这里我只看一下效果,后面代码里面会循环调用;
Context 上下文
作用是用来处理data数据,便于查找对应的数据
/**
*
* view 类 数据 比如 {{name: 'zs', age: 14}}
* 把数据作为参数 实例化Context 。便于 创建新的数据 并且根据树结构查找数据
*/
function Context(view, parentContext) {
// view 如果不存在,默认设置为空对象
this.view = view == null ? {} : view;
// 缓存 对象 .
this.cache = {
'.': this.view
};
// 设置父元素为 传入的第二个参数
this.parent = parentContext;
}
/**
* 使用给定视图创建一个新上下文,并将此上下文作为父级。
*/
Context.prototype.push = function (view) {
return new Context(view, this);
};
/**
* 返回此上下文中给定名称的值,如果此上下文视图中缺少该值,则遍历上下文层次结构。
*/
Context.prototype.lookup = function (name) {
// 首先拿到缓存
var cache = this.cache;
// 定义查找的内容
var value;
// 如果要查找的 内容在缓存中 赋值为value
// 如果是循环 的情况,获取 . 这个符号,会直接返回 上面 有缓存对象
if (name in cache) {
value = cache[name];
} else {
// context 初始定义为 当前对象,下面可能会改变
var context = this,
names, index;
// 循环遍历 context ,知道context不存在为止
while (context) {
// 如果要查找的是 a.b.c 类似 的数据
if (name.indexOf('.') > 0) {
// 首先把 数据赋值为value 比如 { a: { b: c: 'haha' } }
value = context.view;
// names 把 a.b.c 拆分为 [a.b.c]
names = name.split('.');
// index 初始为0
index = 0;
// 循环 names 数组 ,并分别从value中查找到值,并重新赋值为value
// 比如上面最后会得到 value 为 haha
while (value != null && index < names.length)
value = value[names[index++]];
}
// 如果 查找的关键字没有点 并且 数据源是对象。那么直接返回匹配的值
else if (typeof context.view == 'object') {
value = context.view[name];
}
// 如果找到匹配值,那么终止循环
if (value != null)
break;
// 如果没有找到匹配的值,那么把context 上下文重新定义为 父 上下文,并重新循环查找
context = context.parent;
}
cache[name] = value;
}
// 如果查找到的内容是 函数,然后执行到,赋值给value
if (isFunction(value))
value = value.call(this.view);
// 返回找到到的内容
return value;
};
Writer
提供render方法。 把模板转换为tokens。然后处理只有的tokens 通过数据转换为浏览器识别的字符串
/**
* 提供解析模板为tokens 然后把tokens 转为 dom字符串
* 根据tokens 转换为 字符串,并且缓存它
*/
function Writer() {
this.cache = {};
}
/**
* 清空缓存
*/
Writer.prototype.clearCache = function () {
this.cache = {};
};
/**
* 解析和缓存给定的“模板”,并返回从解析生成的令牌数组。
*/
Writer.prototype.parse = function (template, tags) {
// 拿到缓存
var cache = this.cache;
// 如果缓存中有,直接返回对应的tokens
var tokens = cache[template];
// 若果缓存中没有。那么调用 parseTemplate返回获取tokens并缓存起来
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* 渲染函数
* template 为模板
* view 为数据
* partials 为补充模板 可以为对象也可以为函数
*/
Writer.prototype.render = function (template, view, partials) {
// 把模板 转化 tokens
var tokens = this.parse(template);
// 数据是不是 是不是 Context的示例,不是的话,用 Context实例化处理数据
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* 递归函数 用于处理tokens 处理为dom字符串
*
* */
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
var buffer = ''; // 初始为字符串
var self = this;
// 附属渲染 获取 顶层渲染的数据
function subRender(template) {
return self.render(template, context, partials);
}
var token, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
// 如果是# 那这个token 就是循环的token
// 比如 ["#", "list", 194, 203, Array(5), 260]
case '#':
// 数据中查找 list 对应的 数据
value = context.lookup(token[1]);
console.log('value', value);
// 如果数据红没有则跳过
if (!value)
continue;
// 如果是 数组
// 比如 {list: [{name:'zs'},{name: 'ls'}]}
// value 为[{name:'zs'},{name: 'ls'}]
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
// 递归 把 Array(5) 作为token, 每一项 {name: 'xx'} 作为数据
buffer += this.renderTokens(token[4], context.push(value[j]), partials,
originalTemplate);
}
// 如果 找到的 value 为对象或字符串
// {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
// value 为 {name:'zs', age:14} 或 'abcd'
} else if (typeof value === 'object' || typeof value === 'string') {
// 也是递归 把 Array(5)作为token, 直接把当前value作为数据
buffer += this.renderTokens(token[4], context.push(value), partials,
originalTemplate);
// 如果找到的函数
/**
* arr: ()=> (template, fn)=>{
* return fn(template)
* }
*
* template == originalTemplate.slice(token[3], token[5]) 为循环内的位解析字符串
* fn == subRender 用根 数据渲染当前模板
*/
} else if (isFunction(value)) {
// 如果 原始模板不是字符串 则报错
if (typeof originalTemplate !== 'string')
throw new Error(
'Cannot use higher-order sections without the original template');
// 返回一个自定义函数, 原函数匹配的字符串为第一个参数 ,subRender为第二个参数(以根数据来渲染当前模板)
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
// 其他情况 直接调用 递归处理 并且不用 context.push反正创建子context
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
break;
case '^':
// 查找到 数据中的对应值 (lookup方法会找到最上层)
value = context.lookup(token[1]);
// 如果没有找到值 或者 是一个空数组 调用 renderTokens 方法查找对应数据
//
if (!value || (isArray(value) && value.length === 0))
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
break;
case '>':
// > 如果没有 分部 partials 跳过
if (!partials)
continue;
// 判断partials 是不是函数,函数执行 否则世界查找partials的 值
value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
// 有值的话继续递归
buffer += this.renderTokens(this.parse(value), context, partials, value);
break;
case '&':
// 如果是& 说明是{{{}}} 中的数据, 去context 中查找到对应的数据 后直接拼接
value = context.lookup(token[1]);
if (value != null)
buffer += value;
break;
case 'name':
// name 去context 中查找到对应的数据
value = context.lookup(token[1]);
// 如果不为空,转义之后拼接
if (value != null)
buffer += mustache.escape(value);
break;
case 'text':
// text 是文本 ,直接拼接
buffer += token[1];
break;
}
}
return buffer;
};
下面是整个文件的注解:
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false*/
// umd 写法: 兼容AMD和commonJS规范的同时,还兼容全局引用的方式
(function (global, factory) {
if (typeof exports === "object" && exports) {
factory(exports); // CommonJS
} else if (typeof define === "function" && define.amd) {
define(['exports'], factory); // AMD
} else {
factory(global.Mustache = {}); // <script>
}
}(this, function (mustache) {
mustache.name = "mustache.js";
mustache.version = "1.0.0";
mustache.tags = ["{{", "}}"]; // 模板的标识
var whiteRe = /\s*/; // 空格可能存在
var spaceRe = /\s+/; // 最少有一个空格
var equalsRe = /\s*=/; // 等于号前面可能存在空格
var curlyRe = /\s*\}/; // 等于号前面最少存在一个空格
var tagRe = /#|\^|\/|>|\{|&|=|!/; // 最少存在匹配里面一个符号
// 导出三个类主要用于测试,但也用于高级用途
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
mustache.escape = escapeHtml; // escape 作用是把特殊符号转移
/*** escape start ****/
var entityMap = {
"&": "&",
"<": "<",
">": ">",
'"': '"',
"'": ''',
"/": '/'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
/*** escape end ****/
/*** 判断是否是数组 -statrt*****/
var Object_toString = Object.prototype.toString;
var isArray = Array.isArray || function (object) {
return Object_toString.call(object) === '[object Array]';
};
/*** 判断是否是数组 -end*****/
/*** 判断是否是函数 -statrt*****/
function isFunction(object) {
return typeof object === 'function';
}
/*** 判断是否是函数 -end*****/
// $&: 与 regexp 相匹配的子串
// escapeRegExp是把匹配到的符号 前加一个反斜杠
/**拓展:
* $1、$2、...、$99: 与 regexp 中的第 1 到第 99 个子表达式相匹配的文本
* $&:与 regexp 相匹配的子串
* $`:位于匹配子串左侧的文本。
* $':位于匹配子串右侧的文本。
* $$:直接量符号。
* */
function escapeRegExp(string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
/**** 正则 isWhitespace 是空白(空格、换行、tab缩进等所有的空白) start *****/
var RegExp_test = RegExp.prototype.test;
function testRegExp(re, string) {
return RegExp_test.call(re, string);
}
var nonSpaceRe = /\S/;
function isWhitespace(string) {
return !testRegExp(nonSpaceRe, string);
}
/**** 正则 isWhitespace 是空白(空格、换行、tab缩进等所有的空白) end *****/
// 实例化 Writer函数
var defaultWriter = new Writer();
/**
* 清除缓存
*/
mustache.clearCache = function () {
return defaultWriter.clearCache();
};
/**
* 把模板处理为tokens
*/
mustache.parse = function (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* 暴露的render 方法为实例化Writer之后的render方法
*/
mustache.render = function (template, view, partials) {
return defaultWriter.render(template, view, partials);
};
// This is here for backwards compatibility with 0.4.x.
// 0.4.x 的方法 多一个send的. 如果send 是函数,需要执行回调函数
mustache.to_html = function (template, view, partials, send) {
var result = mustache.render(template, view, partials);
if (isFunction(send)) {
send(result);
} else {
return result;
}
};
/**
* 处理模板为tokens
*/
function parseTemplate(template, tags) {
// 如果没有传入模板,直接返回空数组
if (!template)
return [];
var sections = []; // 栈(先进后出,后进先出)
var tokens = []; // 获取的tokens (会切割得很细,一个空格也会是一个token)
var spaces = []; // 这个数据会保存 tokens 中 保存的是空格 的位置
var hasTag = false; // 当前行上有{{tag}}吗?
var nonSpace = false; // 当前行中是否有非空格字符?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
// 处理空格 此时 每一项token 都是一个字符串
function stripSpace() {
// 如果有 匹配符号 并且 有个字符
if (hasTag && !nonSpace) {
// 根据 保存的空格 位置, 删除tokens中的空格token
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
// 把 匹配符号 标志重置为fals。 nonSpace为false
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
// 根据传入的符号 设置标签
function compileTags(tags) {
// 如果 匹配符号是 字符串, 说明要用自定义的解析符号
// 比如 <% %> 安装空字符串切个我一个对象 ["<%", ">%"]
if (typeof tags === 'string')
tags = tags.split(spaceRe, 2);
// 如果tags 不是一个数组,或者 是数组,但是不是两位直接报错
if (!isArray(tags) || tags.length !== 2)
throw new Error('Invalid tags: ' + tags);
// 下面以解析符号获取正则表达式
// /\{\{\s*/ 两个左大括号 后可能有空格
openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
// /\s*\}\}/ 两个右大括号 前可能有空格
closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
// /\s*\}\}\}/ 三个右大括号 前可能有空格
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
}
// 处理tags, 如果没有,那就用默认 tags ["{{", "}}"]
compileTags(tags || mustache.tags);
// 得到 Scanner类的示例
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
// 循环遍历 scanner
while (!scanner.eos()) {
// 获取 位置 pos 会根据scan 和scanUtil 变化
start = scanner.pos;
// 找到 {{ 开始符号前的文本
value = scanner.scanUntil(openingTagRe);
// 把获取到的文本处理
if (value) {
// 把文本字符串 从第一个字符开始遍历 比如 "我是中国人 我爱中国"
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
// 获取当前字符串
chr = value.charAt(i);
// 如果当前字符为空字符 当前tokens的长度 放到spaces数组中
// tokens每一项放的都是一个 字符,sapces会记录哪几项是空格
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
// 如果不是 空字符 设置 当前行中没有空格字符
nonSpace = true;
}
// 把当前字符 以 数组放入tokens中 并记录当前位置和结束位置
tokens.push(['text', chr, start, start + 1]);
start += 1;
// 如果有回车
if (chr === '\n')
// 去掉空格
stripSpace();
}
}
// 如果没有 {{ , 说明已经找完了 结束循环
if (!scanner.scan(openingTagRe))
break;
// 如果没有终止,说明有 {{`
hasTag = true;
// 处理 循环 注释等其他 类型
// 根据 /#|\^|\/|>|\{|&|=|!/ 获取 如果没有匹配到,那就是默认的数据类型 name
// 可能是 循环开始# 循环结束\ 注释!不用转义{ 等
type = scanner.scan(tagRe) || 'name';
// 跳过空字符串
scanner.scan(whiteRe);
// 如果type 为 = 符号
if (type === '=') {
// value 保存 新的 匹配符号 比如 {{=<% %>=}} 获取到 value = "<% %>"
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') { // {
// value 或 }}} 前的内容
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
// 并且把类型重置为 & 符号
type = '&';
} else {
// 其他情况 获取结束符号}} 前的文本
value = scanner.scanUntil(closingTagRe);
}
// 匹配结束符号
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
// 设置每一项token [类型, 值, 开始位置, scanner的位置]
token = [type, value, start, scanner.pos];
// 推到tokens中
tokens.push(token);
// 如果 # 或者 ^ 符号 推入 栈sections中
if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// 遇到结束符号 / 把 栈里面取出第一个
openSection = sections.pop();
// 如果没有内容,报错
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
// 如果内容 [type, value, start, scanner.pos] 如果值不相等报错
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
// 当前行中 无空格字符
nonSpace = true;
} else if (type === '=') {
// 为下一次设置标签
compileTags(value);
}
}
// 完成后确保没有打开的部分。
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* 将给定“tokens”数组中连续文本标记的值合并为单个标记。
* 把相邻的token 切 type都是text类型的文本合并为一个token
*/
function squashTokens(tokens) {
var squashedTokens = []; //定义压缩数组
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
// token 存在 [type, value, start, scanner.pos]
if (token) {
// 如果当前token类型为 text 。 并且上一个token(现在的lastToken) 类型也是text
// 那么就把两个token合并
// 这里的处理 把value 现在, scanner.pos 位置为现在token的位置
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
// 第一项 token 存起来, 并且赋值给lastToken
// 或者现在的token和上一个token type不一样
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* 处理 # 开始 的循环接口。处理为嵌套的token
* 把 type 为 # 的开始token ,结束为 '/'结束的token。 这一堆token作为数组,作为#token的第五项
*
* 这里要注意 收集器 的作用主要是 改变 一个指向。如要 # 就要把收集器指向 当前token的 第五项
* 当遇到 / 说明循环结束了。需要把收集器指向上一级的token的第五项
* 循环一直,只到没有循环为止,指向最初的token;
*/
function nestTokens(tokens) {
var nestedTokens = []; // 嵌套tokens
var collector = nestedTokens; // 收集器 指向每一次需要收集的数组
var sections = []; // 栈数组 用于 保存有多少次循环, 并存在循环的主token
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
// 如果type 为 # ^ 那就是嵌套开始项
// [type, value, start, scanner.pos, [嵌套token]]
case '#':
case '^':
// 收集器push 进token
collector.push(token);
// 栈中 推入当前token 这个是类似一个父token
sections.push(token);
// 收集器 现在指向 当前token 第五项并且为空数组
// 只有遇到的token都要放入这个空数组中
collector = token[4] = [];
break;
// 如果遇到/ 那么就是 循环结束了。 需要
case '/':
// 结束了的话,把最后的一次栈中的token 取出来
section = sections.pop();
// 把 当前token 的scanner.pos 给 父token的第六项
section[5] = token[2];
// 判断栈中还有没有数据。没有的话,就是初始nestedTokens;否则,
// 收集器指针 需要指向上一级父token的第五位
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
// 循环中 把每一个token 放入收集器中
collector.push(token);
}
}
// 返回 处理过的循环tokens
return nestedTokens;
}
/**
* 模板解析器用于在模板字符串中查找令牌的简单字符串扫描仪
* 获取字符串,并设置尾部,尾巴会向后减少。pos是当前位置
*/
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* 判断是否到尾部
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* 为了跳过匹配符号
* 返回匹配的匹配的符号,如果没有返回空字符串
* 比如 re为 {{ 找到的话就返回 {{ 并且尾巴向后移动两位,pos位置也加2
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
// 没有匹配直接返回空字符串
if (!match || match.index !== 0)
return '';
var string = match[0];
// 尾巴 向后移动 匹配符号的长度
this.tail = this.tail.substring(string.length);
// 位置加 上匹配符号的长度
this.pos += string.length;
return string;
};
/**
* 为了找到 匹配符号前的文本
* 如果有匹配的符号,返回匹配符号之前的文本,否则就是没有匹配到,返回剩下的尾巴
* 比如 这里是{{name}}的家 re为 {{ 会返回 这里是
*/
Scanner.prototype.scanUntil = function (re) {
// 得到匹配符号在尾巴中的位置(尾巴会变化,所有位置也会在变化)
var index = this.tail.search(re),
match;
switch (index) {
// -1说明没有匹配到特殊符号,说明已经扫描完了。直接返回剩下的文本。比把尾巴置空
case -1:
match = this.tail;
this.tail = "";
break;
// 特殊符号刚好在开始地方,那匹配符号前 就没有文本。匹配到的就是空字符串
case 0:
match = "";
break;
// 其他情况 直接获取匹配符号前的文本,并把尾巴向后移动到匹配的地方(这个时候后面会用scan函数跳过匹配符号)
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
// 位置 需要加上匹配的长度 ,并返回匹配的文本
this.pos += match.length;
return match;
};
/**
*
* view 类 数据 比如 {{name: 'zs', age: 14}}
* 把数据作为参数 实例化Context 。便于 创建新的数据 并且根据树结构查找数据
*/
function Context(view, parentContext) {
// view 如果不存在,默认设置为空对象
this.view = view == null ? {} : view;
// 缓存 对象 .
this.cache = {
'.': this.view
};
// 设置父元素为 传入的第二个参数
this.parent = parentContext;
}
/**
* 使用给定视图创建一个新上下文,并将此上下文作为父级。
*/
Context.prototype.push = function (view) {
return new Context(view, this);
};
/**
* 返回此上下文中给定名称的值,如果此上下文视图中缺少该值,则遍历上下文层次结构。
*/
Context.prototype.lookup = function (name) {
// 首先拿到缓存
var cache = this.cache;
// 定义查找的内容
var value;
// 如果要查找的 内容在缓存中 赋值为value
// 如果是循环 的情况,获取 . 这个符号,会直接返回 上面 有缓存对象
if (name in cache) {
value = cache[name];
} else {
// context 初始定义为 当前对象,下面可能会改变
var context = this,
names, index;
// 循环遍历 context ,知道context不存在为止
while (context) {
// 如果要查找的是 a.b.c 类似 的数据
if (name.indexOf('.') > 0) {
// 首先把 数据赋值为value 比如 { a: { b: c: 'haha' } }
value = context.view;
// names 把 a.b.c 拆分为 [a.b.c]
names = name.split('.');
// index 初始为0
index = 0;
// 循环 names 数组 ,并分别从value中查找到值,并重新赋值为value
// 比如上面最后会得到 value 为 haha
while (value != null && index < names.length)
value = value[names[index++]];
}
// 如果 查找的关键字没有点 并且 数据源是对象。那么直接返回匹配的值
else if (typeof context.view == 'object') {
value = context.view[name];
}
// 如果找到匹配值,那么终止循环
if (value != null)
break;
// 如果没有找到匹配的值,那么把context 上下文重新定义为 父 上下文,并重新循环查找
context = context.parent;
}
cache[name] = value;
}
// 如果查找到的内容是 函数,然后执行到,赋值给value
if (isFunction(value))
value = value.call(this.view);
// 返回找到到的内容
return value;
};
/**
* 提供解析模板为tokens 然后把tokens 转为 dom字符串
* 根据tokens 转换为 字符串,并且缓存它
*/
function Writer() {
this.cache = {};
}
/**
* 清空缓存
*/
Writer.prototype.clearCache = function () {
this.cache = {};
};
/**
* 解析和缓存给定的“模板”,并返回从解析生成的令牌数组。
*/
Writer.prototype.parse = function (template, tags) {
// 拿到缓存
var cache = this.cache;
// 如果缓存中有,直接返回对应的tokens
var tokens = cache[template];
// 若果缓存中没有。那么调用 parseTemplate返回获取tokens并缓存起来
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* 渲染函数
* template 为模板
* view 为数据
* partials 为补充模板 可以为对象也可以为函数
*/
Writer.prototype.render = function (template, view, partials) {
// 把模板 转化 tokens
var tokens = this.parse(template);
// 数据 是不是 Context的实例,不是的话,用 Context实例化处理数据
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* 递归函数 用于处理tokens 处理为dom字符串
*
* */
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
var buffer = ''; // 初始为字符串
var self = this;
// 附属渲染 获取 顶层渲染的数据
function subRender(template) {
return self.render(template, context, partials);
}
var token, value;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
// 如果是# 那这个token 就是循环的token
// 比如 ["#", "list", 194, 203, Array(5), 260]
case '#':
// 数据中查找 list 对应的 数据
value = context.lookup(token[1]);
// 如果数据没有则跳过
if (!value)
continue;
// 如果是 数组
// 比如 {list: [{name:'zs'},{name: 'ls'}]}
// value 为[{name:'zs'},{name: 'ls'}]
if (isArray(value)) {
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
// 递归 把 Array(5) 作为token, 每一项 {name: 'xx'} 作为数据
buffer += this.renderTokens(token[4], context.push(value[j]), partials,
originalTemplate);
}
// 如果 找到的 value 为对象或字符串
// {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
// value 为 {name:'zs', age:14} 或 'abcd'
} else if (typeof value === 'object' || typeof value === 'string') {
// 也是递归 把 Array(5)作为token, 直接把当前value作为数据
buffer += this.renderTokens(token[4], context.push(value), partials,
originalTemplate);
// 如果找到的函数
/**
* arr: ()=> (template, fn)=>{
* return fn(template)
* }
*
* template == originalTemplate.slice(token[3], token[5]) 为循环内的位解析字符串
* fn == subRender 用根 数据渲染当前模板
*/
} else if (isFunction(value)) {
// 如果 原始模板不是字符串 则报错
if (typeof originalTemplate !== 'string')
throw new Error(
'Cannot use higher-order sections without the original template');
// 返回一个自定义函数, 原函数匹配的字符串为第一个参数 ,subRender为第二个参数(以根数据来渲染当前模板)
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
// 其他情况 直接调用 递归处理 并且不用 context.push反正创建子context
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
break;
case '^':
// 查找到 数据中的对应值 (lookup方法会找到最上层)
value = context.lookup(token[1]);
// 如果没有找到值 或者 是一个空数组 调用 renderTokens 方法查找对应数据
if (!value || (isArray(value) && value.length === 0))
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
break;
case '>':
// > 如果没有 分部 partials 跳过
if (!partials)
continue;
// 判断partials 是不是函数,函数执行 否则世界查找partials的 值
value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
// 有值的话继续递归
buffer += this.renderTokens(this.parse(value), context, partials, value);
break;
case '&':
// 如果是& 说明是{{{}}} 中的数据, 去context 中查找到对应的数据 后直接拼接
value = context.lookup(token[1]);
if (value != null)
buffer += value;
break;
case 'name':
// name 去context 中查找到对应的数据
value = context.lookup(token[1]);
// 如果不为空,转义之后拼接
if (value != null)
buffer += mustache.escape(value);
break;
case 'text':
// text 是文本 ,直接拼接
buffer += token[1];
break;
}
}
return buffer;
};
}));