源碼地址:https://cdn.bootcdn.net/ajax/libs/mustache.js/1.0.0/mustache.js
大概的方法
{{name}} 會(huì)把數(shù)據(jù)中 name 轉(zhuǎn)換為具體值
{{#list}} {{/list}} 會(huì)把list中的數(shù)據(jù)選項(xiàng)展示
{{^list}}{{/list}} 數(shù)據(jù)中 list不存在或者未空數(shù)組才展示
{{!注釋}} 注釋
{{{}}} 里面的內(nèi)容不會(huì)轉(zhuǎn)義
{{>name}} 會(huì)從附屬數(shù)據(jù)中獲取數(shù)據(jù)
<div>{{=<% %>=}}</div> 修改 <% %> 為新切割符號(hào)
html測(cè)試
<!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: [], //也可以不要這一項(xiàng)
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模板 通過(guò) 對(duì)應(yīng)的分隔符號(hào) 比如 {{ }} 轉(zhuǎn)為為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 是這個(gè) 類(lèi)型 text 為文本, name需要取數(shù)據(jù)中的鍵 ,# 是循環(huán)
- value 數(shù)據(jù)中的鍵或者字符串
- start 這個(gè)是當(dāng)前數(shù)組所在字符串的位置
- scanner.pos 掃描器的位置或者未結(jié)束位置
然后把tokens處理為 dom識(shí)別的字符串;
源碼里面有三個(gè)類(lèi):
- Scanner;
- Context;
- Writer;
Scanner 掃描器
我的理解 Scanner 是 當(dāng) 匹配符號(hào)比如 {{ 和 }} 匹配一段文本是分別 獲取 匹配符號(hào)之外的文本。(其實(shí)跟以前版本正則類(lèi)似)
比例: 我是{{name}},我愛(ài)我的{{contry}}。也愛(ài)你
通過(guò)Scanner類(lèi)可以獲取到 五段內(nèi)容:
- 我是
- name
- ,我愛(ài)我的
- contry
- 。也愛(ài)你
源碼
/**
* 模板解析器用于在模板字符串中查找令牌的簡(jiǎn)單字符串掃描儀
* 獲取字符串,并設(shè)置尾部,尾巴會(huì)向后減少。pos是當(dāng)前位置
*/
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* 判斷是否到尾部
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* 為了跳過(guò)匹配符號(hào)
* 返回匹配的匹配的符號(hào),如果沒(méi)有返回空字符串
* 比如 re為 {{ 找到的話就返回 {{ 并且尾巴向后移動(dòng)兩位,pos位置也加2
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
// 沒(méi)有匹配直接返回空字符串
if (!match || match.index !== 0)
return '';
var string = match[0];
// 尾巴 向后移動(dòng) 匹配符號(hào)的長(zhǎng)度
this.tail = this.tail.substring(string.length);
// 位置加 上匹配符號(hào)的長(zhǎng)度
this.pos += string.length;
return string;
};
/**
* 為了找到 匹配符號(hào)前的文本
* 如果有匹配的符號(hào),返回匹配符號(hào)之前的文本,否則就是沒(méi)有匹配到,返回剩下的尾巴
* 比如 這里是{{name}}的家 re為 {{ 會(huì)返回 這里是
*/
Scanner.prototype.scanUntil = function (re) {
// 得到匹配符號(hào)在尾巴中的位置(尾巴會(huì)變化,所有位置也會(huì)在變化)
var index = this.tail.search(re),
match;
switch (index) {
// -1說(shuō)明沒(méi)有匹配到特殊符號(hào),說(shuō)明已經(jīng)掃描完了。直接返回剩下的文本。比把尾巴置空
case -1:
match = this.tail;
this.tail = "";
break;
// 特殊符號(hào)剛好在開(kāi)始地方,那匹配符號(hào)前 就沒(méi)有文本。匹配到的就是空字符串
case 0:
match = "";
break;
// 其他情況 直接獲取匹配符號(hào)前的文本,并把尾巴向后移動(dòng)到匹配的地方(這個(gè)時(shí)候后面會(huì)用scan函數(shù)跳過(guò)匹配符號(hào))
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
// 位置 需要加上匹配的長(zhǎng)度 ,并返回匹配的文本
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>
由于這個(gè)類(lèi)向外暴露了。我們可以直接測(cè)試

這里我只看一下效果,后面代碼里面會(huì)循環(huán)調(diào)用;
Context 上下文
作用是用來(lái)處理data數(shù)據(jù),便于查找對(duì)應(yīng)的數(shù)據(jù)
/**
*
* view 類(lèi) 數(shù)據(jù) 比如 {{name: 'zs', age: 14}}
* 把數(shù)據(jù)作為參數(shù) 實(shí)例化Context 。便于 創(chuàng)建新的數(shù)據(jù) 并且根據(jù)樹(shù)結(jié)構(gòu)查找數(shù)據(jù)
*/
function Context(view, parentContext) {
// view 如果不存在,默認(rèn)設(shè)置為空對(duì)象
this.view = view == null ? {} : view;
// 緩存 對(duì)象 .
this.cache = {
'.': this.view
};
// 設(shè)置父元素為 傳入的第二個(gè)參數(shù)
this.parent = parentContext;
}
/**
* 使用給定視圖創(chuàng)建一個(gè)新上下文,并將此上下文作為父級(jí)。
*/
Context.prototype.push = function (view) {
return new Context(view, this);
};
/**
* 返回此上下文中給定名稱的值,如果此上下文視圖中缺少該值,則遍歷上下文層次結(jié)構(gòu)。
*/
Context.prototype.lookup = function (name) {
// 首先拿到緩存
var cache = this.cache;
// 定義查找的內(nèi)容
var value;
// 如果要查找的 內(nèi)容在緩存中 賦值為value
// 如果是循環(huán) 的情況,獲取 . 這個(gè)符號(hào),會(huì)直接返回 上面 有緩存對(duì)象
if (name in cache) {
value = cache[name];
} else {
// context 初始定義為 當(dāng)前對(duì)象,下面可能會(huì)改變
var context = this,
names, index;
// 循環(huán)遍歷 context ,知道context不存在為止
while (context) {
// 如果要查找的是 a.b.c 類(lèi)似 的數(shù)據(jù)
if (name.indexOf('.') > 0) {
// 首先把 數(shù)據(jù)賦值為value 比如 { a: { b: c: 'haha' } }
value = context.view;
// names 把 a.b.c 拆分為 [a.b.c]
names = name.split('.');
// index 初始為0
index = 0;
// 循環(huán) names 數(shù)組 ,并分別從value中查找到值,并重新賦值為value
// 比如上面最后會(huì)得到 value 為 haha
while (value != null && index < names.length)
value = value[names[index++]];
}
// 如果 查找的關(guān)鍵字沒(méi)有點(diǎn) 并且 數(shù)據(jù)源是對(duì)象。那么直接返回匹配的值
else if (typeof context.view == 'object') {
value = context.view[name];
}
// 如果找到匹配值,那么終止循環(huán)
if (value != null)
break;
// 如果沒(méi)有找到匹配的值,那么把context 上下文重新定義為 父 上下文,并重新循環(huán)查找
context = context.parent;
}
cache[name] = value;
}
// 如果查找到的內(nèi)容是 函數(shù),然后執(zhí)行到,賦值給value
if (isFunction(value))
value = value.call(this.view);
// 返回找到到的內(nèi)容
return value;
};
Writer
提供render方法。 把模板轉(zhuǎn)換為tokens。然后處理只有的tokens 通過(guò)數(shù)據(jù)轉(zhuǎn)換為瀏覽器識(shí)別的字符串
/**
* 提供解析模板為tokens 然后把tokens 轉(zhuǎn)為 dom字符串
* 根據(jù)tokens 轉(zhuǎn)換為 字符串,并且緩存它
*/
function Writer() {
this.cache = {};
}
/**
* 清空緩存
*/
Writer.prototype.clearCache = function () {
this.cache = {};
};
/**
* 解析和緩存給定的“模板”,并返回從解析生成的令牌數(shù)組。
*/
Writer.prototype.parse = function (template, tags) {
// 拿到緩存
var cache = this.cache;
// 如果緩存中有,直接返回對(duì)應(yīng)的tokens
var tokens = cache[template];
// 若果緩存中沒(méi)有。那么調(diào)用 parseTemplate返回獲取tokens并緩存起來(lái)
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* 渲染函數(shù)
* template 為模板
* view 為數(shù)據(jù)
* partials 為補(bǔ)充模板 可以為對(duì)象也可以為函數(shù)
*/
Writer.prototype.render = function (template, view, partials) {
// 把模板 轉(zhuǎn)化 tokens
var tokens = this.parse(template);
// 數(shù)據(jù)是不是 是不是 Context的示例,不是的話,用 Context實(shí)例化處理數(shù)據(jù)
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* 遞歸函數(shù) 用于處理tokens 處理為dom字符串
*
* */
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
var buffer = ''; // 初始為字符串
var self = this;
// 附屬渲染 獲取 頂層渲染的數(shù)據(jù)
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]) {
// 如果是# 那這個(gè)token 就是循環(huán)的token
// 比如 ["#", "list", 194, 203, Array(5), 260]
case '#':
// 數(shù)據(jù)中查找 list 對(duì)應(yīng)的 數(shù)據(jù)
value = context.lookup(token[1]);
console.log('value', value);
// 如果數(shù)據(jù)紅沒(méi)有則跳過(guò)
if (!value)
continue;
// 如果是 數(shù)組
// 比如 {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, 每一項(xiàng) {name: 'xx'} 作為數(shù)據(jù)
buffer += this.renderTokens(token[4], context.push(value[j]), partials,
originalTemplate);
}
// 如果 找到的 value 為對(duì)象或字符串
// {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
// value 為 {name:'zs', age:14} 或 'abcd'
} else if (typeof value === 'object' || typeof value === 'string') {
// 也是遞歸 把 Array(5)作為token, 直接把當(dāng)前value作為數(shù)據(jù)
buffer += this.renderTokens(token[4], context.push(value), partials,
originalTemplate);
// 如果找到的函數(shù)
/**
* arr: ()=> (template, fn)=>{
* return fn(template)
* }
*
* template == originalTemplate.slice(token[3], token[5]) 為循環(huán)內(nèi)的位解析字符串
* fn == subRender 用根 數(shù)據(jù)渲染當(dāng)前模板
*/
} else if (isFunction(value)) {
// 如果 原始模板不是字符串 則報(bào)錯(cuò)
if (typeof originalTemplate !== 'string')
throw new Error(
'Cannot use higher-order sections without the original template');
// 返回一個(gè)自定義函數(shù), 原函數(shù)匹配的字符串為第一個(gè)參數(shù) ,subRender為第二個(gè)參數(shù)(以根數(shù)據(jù)來(lái)渲染當(dāng)前模板)
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
// 其他情況 直接調(diào)用 遞歸處理 并且不用 context.push反正創(chuàng)建子context
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
break;
case '^':
// 查找到 數(shù)據(jù)中的對(duì)應(yīng)值 (lookup方法會(huì)找到最上層)
value = context.lookup(token[1]);
// 如果沒(méi)有找到值 或者 是一個(gè)空數(shù)組 調(diào)用 renderTokens 方法查找對(duì)應(yīng)數(shù)據(jù)
//
if (!value || (isArray(value) && value.length === 0))
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
break;
case '>':
// > 如果沒(méi)有 分部 partials 跳過(guò)
if (!partials)
continue;
// 判斷partials 是不是函數(shù),函數(shù)執(zhí)行 否則世界查找partials的 值
value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
// 有值的話繼續(xù)遞歸
buffer += this.renderTokens(this.parse(value), context, partials, value);
break;
case '&':
// 如果是& 說(shuō)明是{{{}}} 中的數(shù)據(jù), 去context 中查找到對(duì)應(yīng)的數(shù)據(jù) 后直接拼接
value = context.lookup(token[1]);
if (value != null)
buffer += value;
break;
case 'name':
// name 去context 中查找到對(duì)應(yīng)的數(shù)據(jù)
value = context.lookup(token[1]);
// 如果不為空,轉(zhuǎn)義之后拼接
if (value != null)
buffer += mustache.escape(value);
break;
case 'text':
// text 是文本 ,直接拼接
buffer += token[1];
break;
}
}
return buffer;
};
下面是整個(gè)文件的注解:
/*!
* mustache.js - Logic-less {{mustache}} templates with JavaScript
* http://github.com/janl/mustache.js
*/
/*global define: false*/
// umd 寫(xiě)法: 兼容AMD和commonJS規(guī)范的同時(shí),還兼容全局引用的方式
(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 = ["{{", "}}"]; // 模板的標(biāo)識(shí)
var whiteRe = /\s*/; // 空格可能存在
var spaceRe = /\s+/; // 最少有一個(gè)空格
var equalsRe = /\s*=/; // 等于號(hào)前面可能存在空格
var curlyRe = /\s*\}/; // 等于號(hào)前面最少存在一個(gè)空格
var tagRe = /#|\^|\/|>|\{|&|=|!/; // 最少存在匹配里面一個(gè)符號(hào)
// 導(dǎo)出三個(gè)類(lèi)主要用于測(cè)試,但也用于高級(jí)用途
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
mustache.escape = escapeHtml; // escape 作用是把特殊符號(hào)轉(zhuǎn)移
/*** escape start ****/
var entityMap = {
"&": "&",
"<": "<",
">": ">",
'"': '"',
"'": ''',
"/": '/'
};
function escapeHtml(string) {
return String(string).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
/*** escape end ****/
/*** 判斷是否是數(shù)組 -statrt*****/
var Object_toString = Object.prototype.toString;
var isArray = Array.isArray || function (object) {
return Object_toString.call(object) === '[object Array]';
};
/*** 判斷是否是數(shù)組 -end*****/
/*** 判斷是否是函數(shù) -statrt*****/
function isFunction(object) {
return typeof object === 'function';
}
/*** 判斷是否是函數(shù) -end*****/
// $&: 與 regexp 相匹配的子串
// escapeRegExp是把匹配到的符號(hào) 前加一個(gè)反斜杠
/**拓展:
* $1、$2、...、$99: 與 regexp 中的第 1 到第 99 個(gè)子表達(dá)式相匹配的文本
* $&:與 regexp 相匹配的子串
* $`:位于匹配子串左側(cè)的文本。
* $':位于匹配子串右側(cè)的文本。
* $$:直接量符號(hào)。
* */
function escapeRegExp(string) {
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
}
/**** 正則 isWhitespace 是空白(空格、換行、tab縮進(jìn)等所有的空白) 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縮進(jìn)等所有的空白) end *****/
// 實(shí)例化 Writer函數(shù)
var defaultWriter = new Writer();
/**
* 清除緩存
*/
mustache.clearCache = function () {
return defaultWriter.clearCache();
};
/**
* 把模板處理為tokens
*/
mustache.parse = function (template, tags) {
return defaultWriter.parse(template, tags);
};
/**
* 暴露的render 方法為實(shí)例化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 的方法 多一個(gè)send的. 如果send 是函數(shù),需要執(zhí)行回調(diào)函數(shù)
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) {
// 如果沒(méi)有傳入模板,直接返回空數(shù)組
if (!template)
return [];
var sections = []; // 棧(先進(jìn)后出,后進(jìn)先出)
var tokens = []; // 獲取的tokens (會(huì)切割得很細(xì),一個(gè)空格也會(huì)是一個(gè)token)
var spaces = []; // 這個(gè)數(shù)據(jù)會(huì)保存 tokens 中 保存的是空格 的位置
var hasTag = false; // 當(dāng)前行上有{{tag}}嗎?
var nonSpace = false; // 當(dāng)前行中是否有非空格字符?
// Strips all whitespace tokens array for the current line
// if there was a {{#tag}} on it and otherwise only space.
// 處理空格 此時(shí) 每一項(xiàng)token 都是一個(gè)字符串
function stripSpace() {
// 如果有 匹配符號(hào) 并且 有個(gè)字符
if (hasTag && !nonSpace) {
// 根據(jù) 保存的空格 位置, 刪除tokens中的空格token
while (spaces.length)
delete tokens[spaces.pop()];
} else {
spaces = [];
}
// 把 匹配符號(hào) 標(biāo)志重置為fals。 nonSpace為false
hasTag = false;
nonSpace = false;
}
var openingTagRe, closingTagRe, closingCurlyRe;
// 根據(jù)傳入的符號(hào) 設(shè)置標(biāo)簽
function compileTags(tags) {
// 如果 匹配符號(hào)是 字符串, 說(shuō)明要用自定義的解析符號(hào)
// 比如 <% %> 安裝空字符串切個(gè)我一個(gè)對(duì)象 ["<%", ">%"]
if (typeof tags === 'string')
tags = tags.split(spaceRe, 2);
// 如果tags 不是一個(gè)數(shù)組,或者 是數(shù)組,但是不是兩位直接報(bào)錯(cuò)
if (!isArray(tags) || tags.length !== 2)
throw new Error('Invalid tags: ' + tags);
// 下面以解析符號(hào)獲取正則表達(dá)式
// /\{\{\s*/ 兩個(gè)左大括號(hào) 后可能有空格
openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
// /\s*\}\}/ 兩個(gè)右大括號(hào) 前可能有空格
closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
// /\s*\}\}\}/ 三個(gè)右大括號(hào) 前可能有空格
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
}
// 處理tags, 如果沒(méi)有,那就用默認(rèn) tags ["{{", "}}"]
compileTags(tags || mustache.tags);
// 得到 Scanner類(lèi)的示例
var scanner = new Scanner(template);
var start, type, value, chr, token, openSection;
// 循環(huán)遍歷 scanner
while (!scanner.eos()) {
// 獲取 位置 pos 會(huì)根據(jù)scan 和scanUtil 變化
start = scanner.pos;
// 找到 {{ 開(kāi)始符號(hào)前的文本
value = scanner.scanUntil(openingTagRe);
// 把獲取到的文本處理
if (value) {
// 把文本字符串 從第一個(gè)字符開(kāi)始遍歷 比如 "我是中國(guó)人 我愛(ài)中國(guó)"
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
// 獲取當(dāng)前字符串
chr = value.charAt(i);
// 如果當(dāng)前字符為空字符 當(dāng)前tokens的長(zhǎng)度 放到spaces數(shù)組中
// tokens每一項(xiàng)放的都是一個(gè) 字符,sapces會(huì)記錄哪幾項(xiàng)是空格
if (isWhitespace(chr)) {
spaces.push(tokens.length);
} else {
// 如果不是 空字符 設(shè)置 當(dāng)前行中沒(méi)有空格字符
nonSpace = true;
}
// 把當(dāng)前字符 以 數(shù)組放入tokens中 并記錄當(dāng)前位置和結(jié)束位置
tokens.push(['text', chr, start, start + 1]);
start += 1;
// 如果有回車(chē)
if (chr === '\n')
// 去掉空格
stripSpace();
}
}
// 如果沒(méi)有 {{ , 說(shuō)明已經(jīng)找完了 結(jié)束循環(huán)
if (!scanner.scan(openingTagRe))
break;
// 如果沒(méi)有終止,說(shuō)明有 {{`
hasTag = true;
// 處理 循環(huán) 注釋等其他 類(lèi)型
// 根據(jù) /#|\^|\/|>|\{|&|=|!/ 獲取 如果沒(méi)有匹配到,那就是默認(rèn)的數(shù)據(jù)類(lèi)型 name
// 可能是 循環(huán)開(kāi)始# 循環(huán)結(jié)束\ 注釋?zhuān)〔挥棉D(zhuǎn)義{ 等
type = scanner.scan(tagRe) || 'name';
// 跳過(guò)空字符串
scanner.scan(whiteRe);
// 如果type 為 = 符號(hào)
if (type === '=') {
// value 保存 新的 匹配符號(hào) 比如 {{=<% %>=}} 獲取到 value = "<% %>"
value = scanner.scanUntil(equalsRe);
scanner.scan(equalsRe);
scanner.scanUntil(closingTagRe);
} else if (type === '{') { // {
// value 或 }}} 前的內(nèi)容
value = scanner.scanUntil(closingCurlyRe);
scanner.scan(curlyRe);
scanner.scanUntil(closingTagRe);
// 并且把類(lèi)型重置為 & 符號(hào)
type = '&';
} else {
// 其他情況 獲取結(jié)束符號(hào)}} 前的文本
value = scanner.scanUntil(closingTagRe);
}
// 匹配結(jié)束符號(hào)
if (!scanner.scan(closingTagRe))
throw new Error('Unclosed tag at ' + scanner.pos);
// 設(shè)置每一項(xiàng)token [類(lèi)型, 值, 開(kāi)始位置, scanner的位置]
token = [type, value, start, scanner.pos];
// 推到tokens中
tokens.push(token);
// 如果 # 或者 ^ 符號(hào) 推入 棧sections中
if (type === '#' || type === '^') {
sections.push(token);
} else if (type === '/') {
// 遇到結(jié)束符號(hào) / 把 棧里面取出第一個(gè)
openSection = sections.pop();
// 如果沒(méi)有內(nèi)容,報(bào)錯(cuò)
if (!openSection)
throw new Error('Unopened section "' + value + '" at ' + start);
// 如果內(nèi)容 [type, value, start, scanner.pos] 如果值不相等報(bào)錯(cuò)
if (openSection[1] !== value)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
} else if (type === 'name' || type === '{' || type === '&') {
// 當(dāng)前行中 無(wú)空格字符
nonSpace = true;
} else if (type === '=') {
// 為下一次設(shè)置標(biāo)簽
compileTags(value);
}
}
// 完成后確保沒(méi)有打開(kāi)的部分。
openSection = sections.pop();
if (openSection)
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
return nestTokens(squashTokens(tokens));
}
/**
* 將給定“tokens”數(shù)組中連續(xù)文本標(biāo)記的值合并為單個(gè)標(biāo)記。
* 把相鄰的token 切 type都是text類(lèi)型的文本合并為一個(gè)token
*/
function squashTokens(tokens) {
var squashedTokens = []; //定義壓縮數(shù)組
var token, lastToken;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
// token 存在 [type, value, start, scanner.pos]
if (token) {
// 如果當(dāng)前token類(lèi)型為 text 。 并且上一個(gè)token(現(xiàn)在的lastToken) 類(lèi)型也是text
// 那么就把兩個(gè)token合并
// 這里的處理 把value 現(xiàn)在, scanner.pos 位置為現(xiàn)在token的位置
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
lastToken[1] += token[1];
lastToken[3] = token[3];
} else {
// 第一項(xiàng) token 存起來(lái), 并且賦值給lastToken
// 或者現(xiàn)在的token和上一個(gè)token type不一樣
squashedTokens.push(token);
lastToken = token;
}
}
}
return squashedTokens;
}
/**
* 處理 # 開(kāi)始 的循環(huán)接口。處理為嵌套的token
* 把 type 為 # 的開(kāi)始token ,結(jié)束為 '/'結(jié)束的token。 這一堆token作為數(shù)組,作為#token的第五項(xiàng)
*
* 這里要注意 收集器 的作用主要是 改變 一個(gè)指向。如要 # 就要把收集器指向 當(dāng)前token的 第五項(xiàng)
* 當(dāng)遇到 / 說(shuō)明循環(huán)結(jié)束了。需要把收集器指向上一級(jí)的token的第五項(xiàng)
* 循環(huán)一直,只到?jīng)]有循環(huán)為止,指向最初的token;
*/
function nestTokens(tokens) {
var nestedTokens = []; // 嵌套tokens
var collector = nestedTokens; // 收集器 指向每一次需要收集的數(shù)組
var sections = []; // 棧數(shù)組 用于 保存有多少次循環(huán), 并存在循環(huán)的主token
var token, section;
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
token = tokens[i];
switch (token[0]) {
// 如果type 為 # ^ 那就是嵌套開(kāi)始項(xiàng)
// [type, value, start, scanner.pos, [嵌套token]]
case '#':
case '^':
// 收集器push 進(jìn)token
collector.push(token);
// 棧中 推入當(dāng)前token 這個(gè)是類(lèi)似一個(gè)父token
sections.push(token);
// 收集器 現(xiàn)在指向 當(dāng)前token 第五項(xiàng)并且為空數(shù)組
// 只有遇到的token都要放入這個(gè)空數(shù)組中
collector = token[4] = [];
break;
// 如果遇到/ 那么就是 循環(huán)結(jié)束了。 需要
case '/':
// 結(jié)束了的話,把最后的一次棧中的token 取出來(lái)
section = sections.pop();
// 把 當(dāng)前token 的scanner.pos 給 父token的第六項(xiàng)
section[5] = token[2];
// 判斷棧中還有沒(méi)有數(shù)據(jù)。沒(méi)有的話,就是初始nestedTokens;否則,
// 收集器指針 需要指向上一級(jí)父token的第五位
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
break;
default:
// 循環(huán)中 把每一個(gè)token 放入收集器中
collector.push(token);
}
}
// 返回 處理過(guò)的循環(huán)tokens
return nestedTokens;
}
/**
* 模板解析器用于在模板字符串中查找令牌的簡(jiǎn)單字符串掃描儀
* 獲取字符串,并設(shè)置尾部,尾巴會(huì)向后減少。pos是當(dāng)前位置
*/
function Scanner(string) {
this.string = string;
this.tail = string;
this.pos = 0;
}
/**
* 判斷是否到尾部
*/
Scanner.prototype.eos = function () {
return this.tail === "";
};
/**
* 為了跳過(guò)匹配符號(hào)
* 返回匹配的匹配的符號(hào),如果沒(méi)有返回空字符串
* 比如 re為 {{ 找到的話就返回 {{ 并且尾巴向后移動(dòng)兩位,pos位置也加2
*/
Scanner.prototype.scan = function (re) {
var match = this.tail.match(re);
// 沒(méi)有匹配直接返回空字符串
if (!match || match.index !== 0)
return '';
var string = match[0];
// 尾巴 向后移動(dòng) 匹配符號(hào)的長(zhǎng)度
this.tail = this.tail.substring(string.length);
// 位置加 上匹配符號(hào)的長(zhǎng)度
this.pos += string.length;
return string;
};
/**
* 為了找到 匹配符號(hào)前的文本
* 如果有匹配的符號(hào),返回匹配符號(hào)之前的文本,否則就是沒(méi)有匹配到,返回剩下的尾巴
* 比如 這里是{{name}}的家 re為 {{ 會(huì)返回 這里是
*/
Scanner.prototype.scanUntil = function (re) {
// 得到匹配符號(hào)在尾巴中的位置(尾巴會(huì)變化,所有位置也會(huì)在變化)
var index = this.tail.search(re),
match;
switch (index) {
// -1說(shuō)明沒(méi)有匹配到特殊符號(hào),說(shuō)明已經(jīng)掃描完了。直接返回剩下的文本。比把尾巴置空
case -1:
match = this.tail;
this.tail = "";
break;
// 特殊符號(hào)剛好在開(kāi)始地方,那匹配符號(hào)前 就沒(méi)有文本。匹配到的就是空字符串
case 0:
match = "";
break;
// 其他情況 直接獲取匹配符號(hào)前的文本,并把尾巴向后移動(dòng)到匹配的地方(這個(gè)時(shí)候后面會(huì)用scan函數(shù)跳過(guò)匹配符號(hào))
default:
match = this.tail.substring(0, index);
this.tail = this.tail.substring(index);
}
// 位置 需要加上匹配的長(zhǎng)度 ,并返回匹配的文本
this.pos += match.length;
return match;
};
/**
*
* view 類(lèi) 數(shù)據(jù) 比如 {{name: 'zs', age: 14}}
* 把數(shù)據(jù)作為參數(shù) 實(shí)例化Context 。便于 創(chuàng)建新的數(shù)據(jù) 并且根據(jù)樹(shù)結(jié)構(gòu)查找數(shù)據(jù)
*/
function Context(view, parentContext) {
// view 如果不存在,默認(rèn)設(shè)置為空對(duì)象
this.view = view == null ? {} : view;
// 緩存 對(duì)象 .
this.cache = {
'.': this.view
};
// 設(shè)置父元素為 傳入的第二個(gè)參數(shù)
this.parent = parentContext;
}
/**
* 使用給定視圖創(chuàng)建一個(gè)新上下文,并將此上下文作為父級(jí)。
*/
Context.prototype.push = function (view) {
return new Context(view, this);
};
/**
* 返回此上下文中給定名稱的值,如果此上下文視圖中缺少該值,則遍歷上下文層次結(jié)構(gòu)。
*/
Context.prototype.lookup = function (name) {
// 首先拿到緩存
var cache = this.cache;
// 定義查找的內(nèi)容
var value;
// 如果要查找的 內(nèi)容在緩存中 賦值為value
// 如果是循環(huán) 的情況,獲取 . 這個(gè)符號(hào),會(huì)直接返回 上面 有緩存對(duì)象
if (name in cache) {
value = cache[name];
} else {
// context 初始定義為 當(dāng)前對(duì)象,下面可能會(huì)改變
var context = this,
names, index;
// 循環(huán)遍歷 context ,知道context不存在為止
while (context) {
// 如果要查找的是 a.b.c 類(lèi)似 的數(shù)據(jù)
if (name.indexOf('.') > 0) {
// 首先把 數(shù)據(jù)賦值為value 比如 { a: { b: c: 'haha' } }
value = context.view;
// names 把 a.b.c 拆分為 [a.b.c]
names = name.split('.');
// index 初始為0
index = 0;
// 循環(huán) names 數(shù)組 ,并分別從value中查找到值,并重新賦值為value
// 比如上面最后會(huì)得到 value 為 haha
while (value != null && index < names.length)
value = value[names[index++]];
}
// 如果 查找的關(guān)鍵字沒(méi)有點(diǎn) 并且 數(shù)據(jù)源是對(duì)象。那么直接返回匹配的值
else if (typeof context.view == 'object') {
value = context.view[name];
}
// 如果找到匹配值,那么終止循環(huán)
if (value != null)
break;
// 如果沒(méi)有找到匹配的值,那么把context 上下文重新定義為 父 上下文,并重新循環(huán)查找
context = context.parent;
}
cache[name] = value;
}
// 如果查找到的內(nèi)容是 函數(shù),然后執(zhí)行到,賦值給value
if (isFunction(value))
value = value.call(this.view);
// 返回找到到的內(nèi)容
return value;
};
/**
* 提供解析模板為tokens 然后把tokens 轉(zhuǎn)為 dom字符串
* 根據(jù)tokens 轉(zhuǎn)換為 字符串,并且緩存它
*/
function Writer() {
this.cache = {};
}
/**
* 清空緩存
*/
Writer.prototype.clearCache = function () {
this.cache = {};
};
/**
* 解析和緩存給定的“模板”,并返回從解析生成的令牌數(shù)組。
*/
Writer.prototype.parse = function (template, tags) {
// 拿到緩存
var cache = this.cache;
// 如果緩存中有,直接返回對(duì)應(yīng)的tokens
var tokens = cache[template];
// 若果緩存中沒(méi)有。那么調(diào)用 parseTemplate返回獲取tokens并緩存起來(lái)
if (tokens == null)
tokens = cache[template] = parseTemplate(template, tags);
return tokens;
};
/**
* 渲染函數(shù)
* template 為模板
* view 為數(shù)據(jù)
* partials 為補(bǔ)充模板 可以為對(duì)象也可以為函數(shù)
*/
Writer.prototype.render = function (template, view, partials) {
// 把模板 轉(zhuǎn)化 tokens
var tokens = this.parse(template);
// 數(shù)據(jù) 是不是 Context的實(shí)例,不是的話,用 Context實(shí)例化處理數(shù)據(jù)
var context = (view instanceof Context) ? view : new Context(view);
return this.renderTokens(tokens, context, partials, template);
};
/**
* 遞歸函數(shù) 用于處理tokens 處理為dom字符串
*
* */
Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
var buffer = ''; // 初始為字符串
var self = this;
// 附屬渲染 獲取 頂層渲染的數(shù)據(jù)
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]) {
// 如果是# 那這個(gè)token 就是循環(huán)的token
// 比如 ["#", "list", 194, 203, Array(5), 260]
case '#':
// 數(shù)據(jù)中查找 list 對(duì)應(yīng)的 數(shù)據(jù)
value = context.lookup(token[1]);
// 如果數(shù)據(jù)沒(méi)有則跳過(guò)
if (!value)
continue;
// 如果是 數(shù)組
// 比如 {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, 每一項(xiàng) {name: 'xx'} 作為數(shù)據(jù)
buffer += this.renderTokens(token[4], context.push(value[j]), partials,
originalTemplate);
}
// 如果 找到的 value 為對(duì)象或字符串
// {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
// value 為 {name:'zs', age:14} 或 'abcd'
} else if (typeof value === 'object' || typeof value === 'string') {
// 也是遞歸 把 Array(5)作為token, 直接把當(dāng)前value作為數(shù)據(jù)
buffer += this.renderTokens(token[4], context.push(value), partials,
originalTemplate);
// 如果找到的函數(shù)
/**
* arr: ()=> (template, fn)=>{
* return fn(template)
* }
*
* template == originalTemplate.slice(token[3], token[5]) 為循環(huán)內(nèi)的位解析字符串
* fn == subRender 用根 數(shù)據(jù)渲染當(dāng)前模板
*/
} else if (isFunction(value)) {
// 如果 原始模板不是字符串 則報(bào)錯(cuò)
if (typeof originalTemplate !== 'string')
throw new Error(
'Cannot use higher-order sections without the original template');
// 返回一個(gè)自定義函數(shù), 原函數(shù)匹配的字符串為第一個(gè)參數(shù) ,subRender為第二個(gè)參數(shù)(以根數(shù)據(jù)來(lái)渲染當(dāng)前模板)
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
if (value != null)
buffer += value;
} else {
// 其他情況 直接調(diào)用 遞歸處理 并且不用 context.push反正創(chuàng)建子context
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
}
break;
case '^':
// 查找到 數(shù)據(jù)中的對(duì)應(yīng)值 (lookup方法會(huì)找到最上層)
value = context.lookup(token[1]);
// 如果沒(méi)有找到值 或者 是一個(gè)空數(shù)組 調(diào)用 renderTokens 方法查找對(duì)應(yīng)數(shù)據(jù)
if (!value || (isArray(value) && value.length === 0))
buffer += this.renderTokens(token[4], context, partials, originalTemplate);
break;
case '>':
// > 如果沒(méi)有 分部 partials 跳過(guò)
if (!partials)
continue;
// 判斷partials 是不是函數(shù),函數(shù)執(zhí)行 否則世界查找partials的 值
value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
if (value != null)
// 有值的話繼續(xù)遞歸
buffer += this.renderTokens(this.parse(value), context, partials, value);
break;
case '&':
// 如果是& 說(shuō)明是{{{}}} 中的數(shù)據(jù), 去context 中查找到對(duì)應(yīng)的數(shù)據(jù) 后直接拼接
value = context.lookup(token[1]);
if (value != null)
buffer += value;
break;
case 'name':
// name 去context 中查找到對(duì)應(yīng)的數(shù)據(jù)
value = context.lookup(token[1]);
// 如果不為空,轉(zhuǎn)義之后拼接
if (value != null)
buffer += mustache.escape(value);
break;
case 'text':
// text 是文本 ,直接拼接
buffer += token[1];
break;
}
}
return buffer;
};
}));