源码地址:https://cdn.bootcdn.net/ajax/libs/mustache.js/1.0.0/mustache.js
大概的方法
{{name}} 会把数据中 name 转换为具体值
{{#list}} {{/list}} 会把list中的数据选项展示
{{^list}}{{/list}} 数据中 list不存在或者未空数组才展示
{{!注释}} 注释
{{{}}} 里面的内容不会转义
{{>name}} 会从附属数据中获取数据
{{=<% %>=}}
修改 <% %> 为新切割符号
html测试
Document
原理
1.0是 大概原理是 把template模板 通过 对应的分隔符号 比如 {{ }} 转为为tokens;
["text", "
↵", 181, 188]
["^", "list", 194, 203, Array(5), 260]
["text", "
姓名:", 204, 221]
["name", "name", 221, 229]
["text", "年龄:", 229, 235]
["name", "age", 235, 242]
["text", "↵", 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
Document
由于这个类向外暴露了。我们可以直接测试
这里我只看一下效果,后面代码里面会循环调用;
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 = {}); //