vue系列—Mustache.js模板引擎介绍及源码解析(十)

根据官方介绍:Mustache可以被用于html文件、配置文件、源代码等很多场景。它的运行得益于扩展一些标签在模板文件中,然后使用一个hash字典或对象对其进行替换渲染操作。

基本语法如下:

  1. {{ keyName }}: 读取属性值, 如果有html标签的话,会被转义。
  2. {{{ keyName }}}: 读取属性值且原样输出,即html不进行转义。
  3. {{ #keyName }} {{ /keyName }}: 用于遍历。
  4. {{ ^keyName }} {{ /keyName }}: 反义数据,当keyName不存在、或为null,或为false时会生效。可以理解相当于我们js中的 !(非)。
  5. {{.}}: 用于遍历数组。
  6. {{ !comments }}: 用于注释。
  7. Partials: 使用可重用的模板,使用方式:{{> 变量}}。

1. 变量 {{ keyName }} 或 {{{ keyName }}}

标签最主要是通过一个变量来使用。比如 {{ keyName }}标签在模板中会尝试查找keyName这个变量在当前的上下文中,如果上下文中不存在keyName变量,那么它会通过递归的方式依次查找它的父级元素,依次类推… 如果最顶级的上下文中依然找不到的话,那么该keyName变量就不会被渲染。否则的话,keyName标签就会被渲染。
如果变量中存在html标签会被转义的。因此如果我们不想html标签转义的话,我们可以使用三个花括号 {{{ keyName }}}.

比如如下列子:
项目基本结构如下:

基本代码如下所示:

如上可以看到,我们name字段,存在a标签中的 < 或 > 被转义了,如果我们想它们不需要转义的话,我们需要使用三个花括号 {{{}}}。如下代码输出:

当然如果我们上面不想使用三个花括号的话,我们也可以使用 & 告诉上下文不需要进行转义。比如 {{ &name }} 这样的,如上面的三个花括号 {{{ name }}}, 我们也可以改成 {{ &name }}; 效果是一样的。

2. 块

2.1 {{#keyName}} {{/keyName}}

{{#keyName}} 是一个标签,它的含义是块的意思。所谓块就是渲染一个区域的文本一次或多次。
块的开始形式是:{{#keyName}},结束形式是:{{/keyName}}。

我们可以使用该 {{#keyName}} {{/keyName}} 标签来遍历一个数组或对象。如下代码所示:

注意:如果上面的 msg 是一个布尔值 false的话,即 msg: false, 那么 tpl 模板不会被渲染。最后html为 ”; 但是如果 msg 的值是 msg: {} 这样的话,那么tpl会渲染,只是没有值而已,最后输出:” 这样的。

Function

当keyName的值是一个可以被调用的对象,或者是一个函数的话,那么该函数会被调用并且传递标签包含的文本进去。如下代码所示:

如果该变量的值也是一个函数的话,那么我们也可以迭代上下文的数组。如下代码演示:

2.2 {{ ^keyName }} {{ /keyName }}

{{ ^keyName }} {{ /keyName }} 的含义是:取相反的数据。当keyName不存在、或为null,或为false时会生效。可以理解相当于我们js中的 !(非) 如下代码所示:

2.3 {{.}}

{{.}} 也是可以遍历一个数组。

如下代码:

3. {{ !comments }}

{{ !comments }} 可以理解为代码注释。良好的编码习惯,都会有一些注释来辅佐。同样在我们的 mustache中也存在注释的标签。
下面我们来看看如何使用注释:

如下代码:

4. Partials的使用

Partials的含义是:使用可重用的模板,使用方式:{{> 变量}}. 相当于 include 的意思。

可以查看如下demo演示:

5. 设置分割符号

有些时候我们想修改一下 mustache默认的标签分割符号 {{}}. mustache也允许我们这样做的。并且修改的方法很简单。
比如说我们把分隔符改成 {% %} 这样的 ,或者 {{% %}}这样的,也是可以的。我们只需要 Mustache.render 方法中传递第四个参数,并且模板也需要改成这样的分割符号,如下代码所示:

如上可以看到,我们在 Mustache.render 方法中,传递了第四个参数为 [ ‘{{%’, ‘%}}’ ],因此在模板中我们的开始标签需要使用 ‘{{%’这样的,在结束标签使用 ‘%}}’ 这样的即可。或者改成任何其他自己喜欢的分隔符都可以,关键设置第四个参数和模板要对应起来。

二:Mustache.js 源码分析

我们首先引入 mustache库文件后,然后我们在页面上打印 console.log(Mustache); 看到打印如下信息:

如上我们可以看到我们的 Mustache.js 库对外提供了很多方法。下面我们来分析下源码:

1. 入口结构如下:

如上代码内部的一些工具函数,稍微了解下就好。及把很多函数挂载到 mustache对外暴露的对象上。因此我们上面打印 console.log(Mustache); 就可以看到 该对象下有很多方法和属性,如上就是对外暴露的。

下面我们可以根据demo来分析,如下demo代码:

从上面我们打印的 console.log(Mustache) 可知:该全局变量有很多方法,其中就有一个 render方法,该方法接收4个参数,如下代码:Mustache.render(tpl, data, ,partials, tags); 各个参数含义分别如下:tpl(模板),data(模板数据),partials(可重用的模板), tags(可自定义设置分隔符);

如上我们只传入两个参数,其中 tpl 是必须传递的参数,否则不传会报错。因此会调用内部 render() 方法,方法代码如下所示:

然后返回 defaultWriter.render(template, view, partials, tags); 函数,defaultWriter 是 Writer方法的实列,因此它有Writer对象中所有的属性和方法。从源码中如下代码可知:

Write 函数原型上有如下方法:

下面我们最主要看 Writer.prototype.render 中的方法吧,代码如下所示:

如上代码,我们首先会调用 this.parse(template, tags); 方法来解析该模板代码; 那么我们就继续看 parse 代码如下:

如上代码解析,我们来看下 parseTemplate 函数代码如下:

如上是 parseTemplate 源码,首先会进入 parseTemplate 函数内部,代码依次往下看,我们会看到首先会调用compileTags函数, 该函数有一个参数 tagsToCompile。从源码上下文中可以看到 mustache.tags 默认值为:[ ‘{{‘, ‘}}’ ]; 因此 tagsToCompile = [ ‘{{‘, ‘}}’ ]; 如果 tagsToCompile 是字符串的话,就执行 tagsToCompile = tagsToCompile.split(spaceRe, 2); 这句代码。

注意:其实我觉得这边 typeof tagsToCompile === ‘string’ 不可能会是字符串,如果是字符串的话,那么在 parse 函数内部就会直接报错了,如下代码内部:

如上,如果tags 传值了的话,它一定是一个数组,如果是字符串的话,那么使用 join分隔符会报错的。
如果 tagsToCompile 不是一个数组 或 它的长度 不等于2的话,那么就抛出一个错误。因为开始标签和结束标签必须成对传递。

继续往下看代码:openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + ‘\s*’);

如上代码,首先会调用 escapeRegExp 函数,传递了一个参数 tagsToCompile[0],从上面分析我们知道 tagsToCompile = [ ‘{{‘, ‘}}’ ]; 因此 tagsToCompile[0] = ‘{{‘了。escapeRegExp 函数代码如下:

因此 代码实际就返回了这样的了

$& 含义是 与 regexp 相匹配的子串;那么匹配了被替换的结果就是 “{{“;

因为 它匹配到 “{{“, 匹配到第一个 “{” 的话,结果被替换为 “{“, 同理匹配到第二个的时候 也是 ‘{‘; 因此结果就是:”{{“; 也可以理解对 { 进行字符串转义。
因此 openingTagRe = new RegExp(“{{” + ‘\s‘) = /{{\s/; 接着往下执行代码:closingTagRe = new RegExp(‘\s‘ + escapeRegExp(tagsToCompile[1]));
tagsToCompile[1] 值为 “}}”; 同理 escapeRegExp(tagsToCompile[1]) 结果就变为:”}}”; 因此 closingTagRe = new RegExp(“\s
” + “}}”) = /\s*}}/;

从上面我们可知:openingTagRe 的含义可以理解为 开始标签,因此正则为 /{{\s/ 就是匹配 开始标签 “{{ ” 或 “{{“,后面允许0个或多个空白。因为我们编写html模板的时候会这样写 {{ xxx }} 这样的。 因此 openingTagRe = /{{\s/; 同理可知:closingTagRe 就是闭合标签了,因此正则需要为 /\s}}/; 那么可以匹配结束标签 ” }}” 或 “}}” 这样的了。因此 closingTagRe = /\s}}/;

继续往下执行代码:

继续往下看代码:var scanner = new Scanner(template);

如上代码,会实列化 Scanner 函数,该函数会传递一个 template参数进去,template参数的值为:”{{#msg}} {{.}} * {{name}} {{/msg}}”; 下面我们来看下 Scanner 函数源码如下:

因此可以分别得出如下值:

继续看代码:var start, type, value, chr, token, openSection; 这些变量我们先不管他,然后继续代码往下:

然后就进入了while循环代码了,while (!scanner.eos()) {} 这样的。

eos方法如下所示:该方法的作用就是判断 scanner对象的 tail属性值是否等于空,如果等于空,说明模板数据已经被解析完成了。
如果解析完成了,就跳出while循环。如下代码:

第一次调用 scanner.eos(); 结果返回 false; 因此进入 while循环内部,start = scanner.pos = 0;

1. 第一次while循环

代码初始化调用 value = scanner.scanUntil(openingTagRe); openingTagRe 值为 /{{\s*/; scanUntil函数代码如下:

如上 Scanner.prototype.scanUntil 函数代码可以看到,这里的this指向了 scanner 对象,因此 this.tail = “{{#msg}} {{.}} * {{name}} {{/msg}}”;
re = /{{\s*/; 因此 var index = this.tail.search(re) = 0; 会进入 case 0: 的情况,因此 match = ”; 最后 this.pos += ”.length = this.pos + 0 = 0; 最后返回 match = ”; 因此 value = ”; 因此不会进入下面的 if(value){} 的语句里面,

继续往下代码执行 if (!scanner.scan(openingTagRe)) openingTagRe的值 = /{{\s*/; 函数如下:

  1. if (!scanner.scan(openingTagRe)) {} 调用的时候,openingTagRe 值为 /{{\s/; 因此re的值为 /{{\s/ 此时 this.tail 值为 “{{#msg}} {{.}} * {{name}} {{/msg}}”,re的值为:/{{\s/;
    var match = “{{#msg}} {{.}} * {{name}} {{/msg}}”.match(/{{\s
    /); 因此:

因此 var string = match[0]; 即:string = “{{“; this.tail = this.tail.substring(string.length); this.tail = “{{#msg}} {{.}} * {{name}} {{/msg}}”.substring(2); 最后 this.tail = “#msg}} {{.}} * {{name}} {{/msg}}”;
this.pos += “{{“.length = 2; 返回 return string; 最后返回 “{{“;

继续代码往下执行,看第二点解释:

  1. 在parseTemplate函数中的 type = scanner.scan(tagRe) || ‘name’; 这个代码调用的时候; 此时:this.tail的值为 = “#msg}} {{.}} * {{name}} {{/msg}}”; tagRe 在页面初始化值为 = /#|\^|\/|>|{|&|=|!/; 因此 re = /#|\^|\/|>|{|&|=|!/; 因此 var match = this.tail.match(re) = “#msg}} {{.}} * {{name}} {{/msg}}”.match(/#|\^|\/|>|{|&|=|!/);
    即match的值为如下:

因此 var string = match[0]; 即:string = ‘#’; this.tail = this.tail.substring(string.length); this.tail = “#msg}} {{.}} * {{name}} {{/msg}}”.substring(1);
最后 this.tail = “msg}} {{.}} * {{name}} {{/msg}}”; this.pos += “#”.length = 3; 返回 return string; 最后返回 ‘#’;
最后返回 type 的值为 “#”; 此时的 scanner 的值为:

代码继续往下执行,看下面第三点解释:

  1. 在 parseTemplate函数中的 scanner.scan(whiteRe); 中调用。 whiteRe 在页面初始化的正则为:var whiteRe = /\s*/;
    从上面第二次调用的返回结果来看scanner的值为:

此时:this.tail 的值为 = “msg}} {{.}} * {{name}} {{/msg}}”; re = /\s*/;

Scanner.prototype.scan 函数源码如下(方便查看源码):

因此 match = “msg}} {{.}} * {{name}} {{/msg}}”.match(/\s*/);

因此 var string = match[0]; 即:string = “”; this.tail = this.tail.substring(0) = “msg}} {{.}} * {{name}} {{/msg}}”;
this.pos += 0; this.pos = 3; 返回 return string; 最后返回 “”;
此时的 scanner 的值为:

由上面我们知道 type = “#”; 因此会直接跳到 else 代码内部。

因此 value = scanner.scanUntil(closingTagRe); 执行,看如下代码解释:
函数代码如下:

scanner.scanUntil(closingTagRe);调用的时候;closingTagRe = “/\s}}/”;
因此 re = “/\s
}}/”; 从上面分析我们可以知道,最终 scanner 对象返回的值如下:

因此 此时的 var index = this.tail.search(re) = “msg}} {{.}} * {{name}} {{/msg}}”.search(/\s*}}/) = 3;
因此会进入 default 的情况下;match = this.tail.substring(0, index); match = “msg}} {{.}} * {{name}} {{/msg}}”.substring(0, 3); = “msg”;
因此 this.tail = this.tail.substring(index); this.tail = “msg}} {{.}} * {{name}} {{/msg}}”.substring(3);
最后 this.tail 的值为 = “}} {{.}} * {{name}} {{/msg}}”; this.pos += match.length; 因此 this.pos = 3 + 3 = 6;
最后返回 match; 因此最后就返回 “msg” 字符串了。
此时我们再看下 scanner 的值为如下:

  1. 在 parseTemplate 函数 内部中 if (!scanner.scan(closingTagRe)) 这句代码时候调用。
    此时 scanner 的值如下所示:

Scanner.prototype.scan 函数源码如下(方便查看源码):

此时 this.tail 的值为 = “}} {{.}} * {{name}} {{/msg}}”; re = /\s}}/;
var match = “}} {{.}} * {{name}} {{/msg}}”.match(/\s
}}/);

因此 this.tail = “}} {{.}} * {{name}} {{/msg}}”.substring(2);
最后 this.tail = ” {{.}} * {{name}} {{/msg}}”;
this.pos += “}}”.length = 8;
最后返回 “}}”; 因此此时的 scannel 的值变为如下:

代码继续往下执行, 如下代码:

因此 token = [“#”, “msg”, 0, 8]; tokens = [[“#”, “msg”, 0, 8]];
因为 type = “#”, 因此进入第一个if循环内部。因此 sections = [[“#”, “msg”, 0, 8]];

2. 第二次while循环
此时的 scannel 的值为如下:

因此 start = 8;
继续执行如下代码:

openingTagRe的值为:openingTagRe = /{{\s/; 因此 re = /{{\s/; 执行代码:var index = this.tail.search(re), match;
由上返回的数据可知:this.tail = ” {{.}} * {{name}} {{/msg}}”; 因此 var index = ” {{.}} * {{name}} {{/msg}}”.search(/{{\s*/) = 1;
同理进入default语句内部,因此 match = this.tail.substring(0, index);
match = ” {{.}} * {{name}} {{/msg}}”.substring(0, 1) = ” “;
this.tail = this.tail.substring(index);
this.tail = ” {{.}} * {{name}} {{/msg}}”.substring(1);
最后 this.tail = “{{.}} * {{name}} {{/msg}}”;
this.pos += match.length;
this.pos = 8 + 1 = 9;
最后返回 return match; 返回 ” “;
因此 此时 scanner 的值变为如下:

执行完成后,value 此时的值为 ” “; 因此会进入 if (value) {} 的内部代码。

注意:if(“”) {} 和 if (” “) {} 结果是不一样的。 “”.length = 0; ” “.length = 1; 源码如下(方便代码理解):

因此 chr = ‘ ‘; 调用 isWhitespace(chr); 方法,其实就是调用了 RegExp.prototype.test.call(/\S/, ‘ ‘); 判断 ‘ ‘ 是否是非空白字符,因此返回false,在 isWhitespace 函数内部,使用了 !符号,因此最后返回true。
spaces.push(tokens.length); 从上面代码可知,我们知道 tokens = [[“#”, “msg”, 0, 8]];
因此 spaces = [1]; tokens.push([ ‘text’, chr, start, start + 1 ]); 执行后 tokens的值变为如下:
tokens = [[“#”, “msg”, 0, 8], [‘text’, ‘ ‘, 8, 9]]; start +=1; 因此 start = 9;
如果 chr === ‘\n’; 则执行 stripSpace()方法,这里为false,因此不执行。
继续执行如下代码:

scan 函数代码如下:

因此 re = /{{\s*/; 从上面可知,我们的scanner的值为如下:

继续执行 Scanner.prototype.scan() 函数内部代码:
var match = this.tail.match(re) = “{{.}} * {{name}} {{/msg}}”.match(/{{\s*/);
因此 match的匹配结果如下:

因此 this.tail = “{{.}} * {{name}} {{/msg}}”.substring(2);
最后 this.tail = “.}} * {{name}} {{/msg}}”;
this.pos += string.length; 因此 this.pos = 9 + 2 = 11;
最后返回 return string; 即返回 “{{“;
因此 此时 scanner 的值变为如下:

接着继续执行代码:type = scanner.scan(tagRe) || ‘name’;
tagRe 在页面是定义的正则为:/#|\^|\/|>|{|&|=|!/;
因此又会执行 Scanner.prototype.scan = function scan (re) {}, 代码如下:

由上面可知:

如上代码:match.index === 10; 因此 不等于0;所以就直接返回 ”; 跳出函数,因此 type = ‘name’ 了;
继续执行如下代码:scanner.scan(whiteRe); whiteRe = /\s/;
还是一样执行 Scanner.prototype.scan = function scan (re) {} 函数;
因此 var match = “.}} * {{name}} {{/msg}}”.match(/\s
/);

再接着执行代码 var string = match[0] = ”;
this.tail = this.tail.substring(0) = “.}} * {{name}} {{/msg}}”;
this.pos += string.length = 11 + 0 = 11;
此时 scanner 的值,和上一步的值一样:

最后返回 空字符串 ”;
如上我们知道 type = ‘name’; 因此 继续进入如下else代码:

再来看下 scanUntil 代码如下:

如上代码:closingTagRe = /\s}}/;
var index = this.tail.search(re) = “.}} * {{name}} {{/msg}}”.search(/\s
}}/) = 1;
因此进入 default语句内部。
因此 match = this.tail.substring(0, index) = “.}} * {{name}} {{/msg}}”.substring(0, 1);
match = ‘.’;
this.tail = this.tail.substring(index) = “.}} * {{name}} {{/msg}}”.substring(1);
因此 this.tail = “}} * {{name}} {{/msg}}”;
this.pos += match.length = 11 + 1 = 12; 最后 return match; 返回 ‘.’;
此时 scanner的值为如下:

接着继续执行 if (!scanner.scan(closingTagRe)){} 代码; closingTagRe = /\s*}}/;

因此调用 Scanner.prototype.scan() 函数后,

因此 this.tail = “}} * {{name}} {{/msg}}”.substring(2);
最后 this.tail = ” * {{name}} {{/msg}}”;
this.pos = 12 + 2 = 14;
最后 return string; 返回 “}}”;
此时 scanner的值为如下:

继续执行代码:token = [ type, value, start, scanner.pos ];
因此 token = [‘name’, ‘.’, 9, 14];
继续往下执行代码:
tokens.push(token);

此时 type = ‘name’; 因此 nonSpace = true; 执行完成后。继续while循环。

第三次while循环

此时scanner值为如下:

start = scanner.pos; 因此 start = 14;
value = scanner.scanUntil(openingTagRe); 执行这句代码:
openingTagRe = /{{\s*/;

var index = this.tail.search(re) = ” * {{name}} {{/msg}}”.search(/{{\s*/);
因此 var index = 8;
然后又继续进入 default语句;此时 match = this.tail.substring(0, index);
match = ” * {{name}} {{/msg}}”.substring(0, 8) = ” * “;
this.tail = this.tail.substring(index) = ” * {{name}} {{/msg}}”.substring(8);
因此 this.tail = “{{name}} {{/msg}}”;
this.pos += match.length = 14 + 8 = 22;
因此 此时scanner值为如下:

最后返回 ” * ” 赋值给 value;
因此继续进入 if (value) {} 代码内部:

继续执行代码:if (!scanner.scan(openingTagRe)) {}; 因此进入 Scanner.prototype.scan = function scan (re) {} 函数代码内部。源码如下:

re 值 = /{{\s*/; 此时 scanner 值为如下:

因此 var match = “{{name}} {{/msg}}”.match(/{{\s*/);

var string = match[0]; 因此 var string = “{{“;
this.tail = this.tail.substring(string.length) = “{{name}} {{/msg}}”.substring(2);
因此:this.tail = “name}} {{/msg}}”;
this.pos += string.length; this.pos = 22 + 2 = 24; 最后返回 “{{“. 因此此时 scanner的值变为如下:

继续执行代码:type = scanner.scan(tagRe) || ‘name’;
函数代码如下:

如上:tagRe = /#|\^|\/|>|{|&|=|!/; this.tail = “name}} {{/msg}}”;
因此 var match = “name}} {{/msg}}”.match(/#|\^|\/|>|{|&|=|!/);

由于 match.index !== 0; 因此直接 返回 ”; 此时 type = ‘name’;
继续执行代码:scanner.scan(whiteRe); whiteRe 的值 = /\s/; 然后又调用 Scanner.prototype.scan 函数。
因此 var match = “name}} {{/msg}}”.match(/\s
/);

最后 返回 return string; 返回 “”;
此时 scanner 的值变为如下:

由于上面 type = “name”; 因此 就会执行 else 语句代码,因此 执行 value = scanner.scanUntil(closingTagRe);

因此继续调用 Scanner.prototype.scanUntil 函数。
因此 var index = “name}} {{/msg}}”.search(/\s*}}/) = 4;
因此 继续进入 default语句代码;
match = “name}} {{/msg}}”.substring(0, 4);
match = “name”;
this.tail = “name}} {{/msg}}”.substring(4) = “}} {{/msg}}”;
this.pos += match.length = 24 + 4 = 28;
最后我们返回 return “name”; 此时 scanner 的值变为如下:

因此 value = “name”;
继续执行下面的代码:
if (!scanner.scan(closingTagRe)) {};
又会调用 Scanner.prototype.scan = function scan (re) {} 函数代码了。
代码如下:

因此值分别为如下:

最后我们返回 “}}”. 因此此时 scanner 的值变为如下:

继续执行代码: token = [ type, value, start, scanner.pos ]; 代码;因此token的值为如下:
token = [‘name’, ‘name’, 22, 30];
继续执行 tokens.push(token); 因此 tokens的值变为如下:

由于 type = “name”; 因此 nonSpace = true;

第四次while循环

调用 scanUntil 函数代码如下:

因此 进入 default 语句代码:
match = this.tail.substring(0, index) = ” {{/msg}}”.substring(0, 7);
因此 match = ” “;
this.tail = this.tail.substring(index) = ” {{/msg}}”.substring(7);
this.tail = “{{/msg}}”;
this.pos += match.length = 30 + 7 = 37;
最后返回 return match; 因此 返回 ” “;
此时 scanner 的值变为如下:

因此 value = ” “;
因此会进入 if (value) {}; 语句代码,如下所示:

由于value的值为 ” “; 长度为7. 因此会在内部for循环中循环7次。依次看下:

接着执行代码: if (!scanner.scan(openingTagRe)) { break; }
openingTagRe 值为:openingTagRe = /{{\s*/;
Scanner.prototype.scan() 函数代码如下:

由上可知,此时 scanner 的值为 = {
pos: 37,
tail: “{{/msg}}”,
string: “{{#msg}} {{.}} * {{name}} {{/msg}}”
};
因此 var match = “{{/msg}}”.match(/{{\s*/);

最后返回 return string; 就返回了 “{{“; 此时 scannel的值变为如下:

再执行代码:type = scanner.scan(tagRe) || ‘name’;
tagRe 的值 = /#|\^|\/|>|{|&|=|!/; 因此会继续调用 Scanner.prototype.scan = function scan (re) {} 函数。
继续执行该函数内部的代码,因此:

最后返回 “/”; 因此 type = ‘/’;
此时 scanner 的值为如下:

再执行代码:scanner.scan(whiteRe); whiteRe 的值 = /\s*/;
因此又进入 scan函数内部依次执行如下:

最后返回 return string; 返回 “”; 跳出该函数,继续执行下一步代码。

因此代码执行到 if (type === ‘=’) {} else if(type === ‘{‘) {} else {} 这里了
由于 type = ‘/’; 因此代码执行到 else 内部了。
执行代码:value = scanner.scanUntil(closingTagRe);
closingTagRe 值为 = /\s*}}/;
Scanner.prototype.scanUntil 函数如下:

因此 var index = “msg}}”.search(/\s*}}/) = 3;
执行到 default语句代码内。
因此 match = this.tail.substring(0, index) = “msg}}”.substring(0, 3) = “msg”;
this.tail = “msg}}”.substring(3) = “}}”;
this.pos += match.length; this.pos = 40 + 3 = 43;
最后返回 return match; 因此 返回 “msg”; 此次此刻 scanner的值变为如下:

现在跳出 Scanner.prototype.scanUntil 函数代码,执行下一步代码:如下:
if (!scanner.scan(closingTagRe)) {}; 因此会调用 scan 函数代码。
closingTagRe 值为 = /\s*}}/;

因此 var match = “}}”.match(/\s*}}/);

最后返回 return string; 因此返回 “}}”. 此时scanner的值变为如下:

跳出 Scanner.prototype.scan 函数代码后,接着执行下面的代码:
token = [ type, value, start, scanner.pos ];
tokens.push(token);
因此 token = [‘/’, ‘msg’, 37, 45];

此时此刻 type = ‘/’; 因此 会执行

else if 内部的代码,如上所示:执行一些清空操作。

我们会调用 nestTokens()函数,在调用该函数之前会调用 squashTokens(tokens);
squashTokens 函数代码如下:

由上面的一系列操作,我们知道tokens的值为如下:

首先在函数内部定义一个新数组 var squashedTokens = []; 然后循环传进来的tokens的值。tokens的长度为20.

为了更清楚的理解具体做了哪些事情,我们继续一步步把for循环拆开理解。如下所示:

最后我们的代码返回 return squashedTokens;

最后一步我们就是要调用 nestTokens 函数了,函数代码如下:

tokens 值就是我们上面返回 return squashedTokens;值了,因此:

然后for循环遍历,因为数组的长度为7,因此循环遍历7次。再分别看下:

现在我们要调用 new Context(view) 方法了。

Context 函数有如下方法:

因此 context.view = { “name”: “kongzhi”, “msg”: [‘111’] };
context.cache = { ‘.’ : { “name”: “kongzhi”, “msg”: [‘111’] } };
context.parent = undefined;
context 实列除了上面列举的三个属性外,在原型上还有 push 和 lookup方法。

接下来就执行:this.renderTokens(tokens, context, partials, template); 代码;
如上参数:

现在我们继续来看下 renderTokens 函数,代码如下:

由上可知:tokens = [[“#”, “msg”, 0, 8, [ [“text”, ” “, 8, 9], [“name”, “.”, 9, 14], [“text”, ” * “, 14, 22], [“name”, “name”, 22, 30], [“text”, ” “, 30, 37] ] , 37 ]];

因此循环tokens, 因此 token = tokens[i] = [“#”, “msg”, 0, 8, [ [“text”, ” “, 8, 9], [“name”, “.”, 9, 14], [“text”, ” * “, 14, 22], [“name”, “name”, 22, 30], [“text”, ” “, 30, 37] ] , 37 ];

因此 symbol = “#”; 因此就调用 renderSection 函数,调用完成后,返回的值赋值给value值,因此下面我们来看下 renderSection 函数的代码如下所示:

如上代码,首先会调用 var value = context.lookup(token[1]); lookup函数,然后把值赋值给 value. 那么此时传递的 token[1] = ‘msg’; 因此我们下面先看下 context.lookup(token[1]) 函数,代码如下所示:

如上函数参数 name 值为 = “msg”;
var cache = this.cache; 我们之前保存的 this.cache 的值为 = { “.”: {name: ‘kongzhi’, msg: [‘111’]}};
很明显 if (cache.hasOwnProperty(name)) {} 为false, 因此进入else语句了。
在else语句代码如下:
var context = this; 这里的this指向了 Context 对象。之前Context值如下所示:

因此 context 也是这个值了。
执行while语句 判断 while(context) {}; 然后判断 if (name.indexOf(‘.’) > 0) {} else {} 这样的,我们上面name为
字符串 “msg”; 因此进入else语句代码内部,intermediateValue = context.view[name]; 因此 intermediateValue = [‘111’]; lookupHit = hasProperty(context.view, name); 判断 context.view 对象内部是否有 “msg” 这个属性,因此lookupHit = true; 然后执行如下代码:

因此 value = [‘111’]; 跳出while循环,当然 如果 lookupHit 为false的话,它会通过递归的方式查找父级元素,直到最顶层
元素,从这句代码可以看到:context = context.parent; 最后跳出while循环后,执行下面的代码 cache[name] = value;
因此这个时候 this.cache = { “.”: {“name”: “kongzhi”, “msg”: [“111”]}, “msg”: [“111”]};
最后判断,我们的 value 是否是个函数,如果是函数的话,就调用该函数执行。我们上面的demo就可以很好的列子,demo如下:

代码: if (isFunction(value)) { value = value.call(this.view);} 我们demo如上,wrapped 就是一个函数,因此它会调用执行,其中 this.view 参数的值为 = { “name”: “kongzhi”, “msg”: [“111”] }; wrapped函数它自身返回一个函数,它会把值赋值给value,因此把value返回回去。因此 又回到 Writer.prototype.renderSection 函数内部,该函数内部又会判断该value 是否是一个数组,如果是个数组的话,执行某些操作,或者 他是一个对象、一个数字、一个字符串、就执行另外一些操作,或者它是一个函数就调用该函数。如下 Writer.prototype.renderSection 函数代码可知:

因此 我们上面 value 返回的是 = [‘111’]; 他是一个数组。因此会进入第一个if语句代码内部。因此循环该数组,由于该 value
数组里面只有 [‘111’] 这样的,因此就循环1次,会执行如下代码:
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);

在调用该函数之前,会调用 context.push 函数,代码如下:

这里的this就是我们之前的 context对象了。因此此时的 context对象就变为如下值:

我们之前的token值如下:

因此调用 buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);

继续递归调用该函数,那么 token[4] = [ [“text”, ” “, 8, 9], [“name”, “.”, 9, 14], [“text”, ” * “, 14, 22], [“name”, “name”, 22, 30], [“text”, ” “, 30, 37] ];

context 值就是我们上面的值;
partials: 可重用的模板,目前没有传递给参数,因此为undefined。
originalTemplate: 就是我们的模板,值为:”{{#msg}} {{.}} * {{name}} {{/msg}}”;

如上我们把token[4] 作为参数传递进去,因此会循环该数组,然后 tokens[][0] === ‘text’ 只有该数组内的第一项,第三项,
第五项,当 tokens[i][0] === ‘name’; 只有数组中的第二项和第四项。我们来分别来看下代码如何执行的;

  1. 第一个数组值为:[“text”, ” “, 8, 9]; 因此:value = this.rawValue([“text”, ” “, 8, 9]); rawValue 函数代码如下:

Writer.prototype.rawValue = function rawValue (token) {
return token[1];
};

该函数直接 返回 token[1]; 因此 value = ” “; 因此第一次循环 buffer = ” “;

  1. 第二个数组值为:[“name”, “.”, 9, 14]; 因此 value = this.escapedValue(token, context); escapedValue 函数代码:

如上代码可以看到,我们的token[1] = ‘.’; 因此先调用 Context.prototype.lookup 这个方法,该方法内部的this指向了
context 对象,我们可以从上面分析可以知道 context对象有哪些值了。因此 this.cache = context.cache = {“.”: “111”};
因此 if (cache.hasOwnProperty(name)) {} 条件为true,因此 value = cache[name]; 即:value = “111”;
最后返回 Writer.prototype.escapedValue 函数内部代码,
if (value != null) {
return mustache.escape(value);
};
从源码当中我们知道:

最后就会返回一个字符串,从该代码中,我们也可以看到,如果模板中有

  1. 第三个数组为:[“text”, ” * “, 14, 22]; 因此:value = this.rawValue([“text”, ” * “, 14, 22]); rawValue 函数代码如下:
    Writer.prototype.rawValue = function rawValue (token) {
    return token[1];
    };
    因此 value = ” * “;
    最后 buffer = ” 111 * “;

  2. 第四个数组为:[“name”, “name”, 22, 30]; 因此 value = this.escapedValue(token, context); 步骤和第二步一样的。
    escapedValue 函数代码(为了方便查看,继续贴下代码):

如上代码,执行 var value = context.lookup(token[1]); 因此 token[1] = “name”; 由上可知:cache = {“.”: “111”};
因此代码会执行else代码内部;进入 while循环内部,if (name.indexOf(‘.’) > 0) {} 判断 “name” 是否能找到 “.”, 这里是找不到的,因此又进去else代码内部。因此 intermediateValue = context.view[name] = context.view[“name”]; 由上面的context的值我们可知,context.view = “111”; 因此 intermediateValue = undefined; 找不到该值;同理 lookupHit = false; 因此 context = context.parent; 查找父级元素,依次类推…. 由上面可知,我们知道 context的值了, 继续看下context 值吧,如下所示:

我们执行 context = context.parent; 它是有的,因此会继续进入下一次while循环代码,因此此时的context值就变为如下了:

和上面的操作一样,也会进入else语句代码内,现在需要执行如下代码:
intermediateValue = context.view[name]; lookupHit = hasProperty(context.view, name);
因此 intermediateValue = context.view[“name”] = “kongzhi”; lookupHit = true; 因此会执行如下代码:

最后我们的 value = “kongzhi” 了,使用break语句,跳出while循环,如上可以看到,如果我们这一次又没有找到该值的话,它还会继续往它的父级元素上面的递归查找是否有该值,如果有直到找到为止,否则的话,就找不到。直接返回 name 这个未解析的变量。
跳出while循环后,就执行 cache[name] = value; 因此这个时候 cache 的值变为如下:

此时此刻,我们全局的context的值就变为如下了:

下面还有如下代码需要执行:

如上,如果该value是一个函数的话,就会返回一个函数给value;否则的话,直接把值value返回给回去。因此我们需要跳到Writer.prototype.escapedValue 函数中,如下代码:

返回回来的value = “kongzhi”; 因此会调用 mustache.escape(value); 函数返回回去,escape 函数我们之前讲过,它是对html标签进行转义的,因此这里为了节约篇幅,就不贴代码了。执行完成后,把值返回回去。因此我们现在又需要跳到Writer.prototype.renderTokens 函数中,再看剩下的代码了,为了查看方便,我继续贴下该函数代码:

因此 执行下面代码 if (value !== undefined) { buffer += value; } 代码了,从上面第三步我们知道 buffer的值了。
buffer = ” 111 * “; 因此我们继续字符串拼接,buffer += value; 因此 buffer = ” 111 * kongzhi”;

  1. 第五个数组为:token = [“text”, ” “, 30, 37]; 由于 token[0] = “text”; 因此:value = this.rawValue([“text”, ” * “, 14, 22]); rawValue 函数代码如下:

Writer.prototype.rawValue = function rawValue (token) {
return token[1];
};
因此 value = ” “; 最后我们又判断 if (value !== undefined) { buffer += value; }; 在第四步我们知道
buffer的值 = ” 111 * kongzhi”; 因此 该值继续和 ” ” 字符串拼接的话,最后我们的buffer值就为如下了:

buffer = ” 111 * kongzhi “;

如上就是整个模板的解析的过程。当然 mustache.js 里面还有很多未讲解到的代码,比如兼容到一些其他的情况。比如在:
Writer.prototype.renderTokens 函数中,还有如下:

如上我们还有三个函数没有讲解到,比如 symbol === ‘^’ 需要调用 this.renderInverted()函数,symbol === ‘>’ 需要调用this.renderPartial()函数,symbol === ‘&’ 需要调用 this.unescapedValue() 这个函数。我们可以贴下他们的代码如下:

如上代码原理也是一样的,这里就不做一一分析了,觉得有意思的,可以自己分析下。到这里源码是分析完了。我们可以从头一步步去理解下mustache.js 模板引擎如何解析的思路。

Original: https://www.cnblogs.com/tugenhua0707/p/11762948.html
Author: 龙恩0707
Title: vue系列—Mustache.js模板引擎介绍及源码解析(十)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/553792/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球