mustache源码解读

1. mustache 以前js大批量生成 html的方式
// 1. dom 
for(let i = 0; i < 10 ; i++) {
    const div = document.createElement('div')
    div.innerHTML = i
    document.body.appendChild(div)
}
// 2. 字符传拼接
let htmlStr = ''
for(let i = 0; i < 10; i++) {
    htmlStr += '<div>' + i + '</div>'
}
document.body.innerHTML = htmlStr
// 3. join
let tagArr = []
let data = [
    {
        name: 'zzzz',
        age: 18
    },
    {
        name: 'xxxx',
        age: 20
    }
]
for(let i = 0; i < data.length; i++) {
    const token = data[i]
    tagArr.push(
        `<div>
			<strong>name: ${token['name']}</strong>
			<em>age: ${token['age']}</em>
		</div>`
    )
}
document.body.innerHTML = tagArr.join('')
2. mustache出现
<script src="./mustache.js"></script>
<script>
    const template = `
	{{#people}}
	<ul>
		<li>
        	name:{{name}}
		</li>
		<li>
        	age:{{age}}
		</li>
	</ul>
	{{/people}}
	`
    const view = {
        people: [
            {
                name: 'zzzz',
                age: 20
            },
            {
                name: 'xxx',
                age: 18
            },
            {
                name: 'yyy',
                age: 111
            }
        ]
    }
   document.body.innerHTML = Mustache.render(template, view)
</script>

mustache 无疑是一个质的飞跃,这样的思维模式也被用在了当下流行的框架当中

那么就让我来分享我在阅读mustache的理解吧

3. 源码分析

首先我们可以发现我们之前调用的Mustache.render 实际上是调用了 defaultWriter.render, 这个时候继续网上看,我们会看到 defaultWriter = new Writer(), 同样的这个时候回到 暴露出来的 mustache对象 ,我们可以看到

mustache.escape = escapeHtml;
mustache.Scanner = Scanner;
mustache.Context = Context;
mustache.Writer = Writer;
以及
var mustache = {
    tags: [ '{{', '}}' ],
    escape: undefined,
    Scanner: undefined,
    Context: undefined,
    Writer: undefined
}

这时我们就可以开始解剖 mustache 的源码了

3.1 功能解析
//进入到Writer中,我们可以看到一下代码
Writer.prototype.render = function render (template, view, partials, config) {
    var tags = this.getConfigTags(config); // tags 配置 用于scanner 的 标识tag 例如 {{
    var tokens = this.parse(template, tags);
    var context = (view instanceof Context) ? view : new Context(view, undefined);
    return this.renderTokens(tokens, context, partials, template, config);  // 简单的认识 只需要关注前两个参数
 };
//我们着重观察 template 和 view 的去向, 发现 由 他们生成了  tokens  和 context,确实最后也是 调用了
//this.renderTokens(tokens, context) 返回了最终的结果
// 那从这里我们 就 引出了 this.parse 和 Context
3.2 this.parse 到底为何方神圣

看到下方代码,提取出我们需要的关键所在, 显然当我们第一次 解析传入的模板时是不可能存在 tokens的,那么parseTemplate又是什么呢?带着这个疑问,我们继续探索

Writer.prototype.parse = function parse (template, tags) {
    //var cache = this.templateCache;
    //var cacheKey = template + ':' + (tags || mustache.tags).join(':');
    
    //var isCacheEnabled = typeof cache !== 'undefined';
    var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;

    if (tokens == undefined) { // 保存解析 token
      tokens = parseTemplate(template, tags);   // parseTemplate
      isCacheEnabled && cache.set(cacheKey, tokens);
    }
    return tokens;
  };

但是当我们找到了它,我们发现这居然是一个100+行的代码块显然对于我这种源码阅历不充分的小萌新不是很友好,这个时候我们需要划分一下结构,我们看到它主要的组成应该是下面的一个结构,阅读 stripSpace函数,compileTags我们可以发现它们分别是用于去除空格,和tag解析的,默认情况 我们使用 mustache.tags = [ ‘{{’, ‘}}’ ],所以这里我们将注意点放在 scanner ,squashTokens,nestTokens上

-parseTemplate

​ -stripSpace

​ -compileTags

​ -scanner

​ -while循环

​ -squashTokens

​ -nestTokens

3.2.1 Scanner解读
//每次调用parseTemplate都会有  var scanenr = new Scanner,因此我们需要对Scanner一探究竟
/**
   * A simple string scanner that is used by the template parser to find
   * tokens in template strings.
   * 阅读描述我们发现scanner的作用是在于将传入字符串转换为 tokens arr,接着我们去看它的具体实现
*/
function Scanner (string) {
  this.string = string; //需要解析的所有字符串
  this.tail = string; //需要继续解析的字符串
  this.pos = 0;
}
/**
   * Returns `true` if the tail is empty (end of string).
*/
// 判断扫描是否结束
Scanner.prototype.eos = function eos () {
  return this.tail === '';
};

/**
   * Tries to match the given regular expression at the current position.
   * Returns the matched text if it can match, the empty string otherwise.
*/
//用正则匹配扫描部分
// 用于跳过 {{}} <% ... 等边界符号
  Scanner.prototype.scan = function scan (re) {
    var match = this.tail.match(re);
    if (!match || match.index !== 0) //字符串index 0-n 与re匹配
      return '';
    var string = match[0];
    this.tail = this.tail.substring(string.length); // 跳过扫描部分
    this.pos += string.length;  // 移动指针
    return string;
  };
/**
   * Skips all text until the given regular expression can be matched. Returns
   * the skipped string, which is the entire tail if no match can be made.
*/
/**
例如  "1243243453453{{name}}"
输入正则匹配 {{
那么我们需要返回1243243453453
*/
  Scanner.prototype.scanUntil = function scanUntil (re) {
    var index = this.tail.search(re), match;
    switch (index) {
      case -1:	//整个字符串都不存在 匹配正则的字符 / 字符串
        match = this.tail;
        this.tail = '';
        break;
      case 0:
        match = '';
        break;
      default: //找到匹配位置
        match = this.tail.substring(0, index);
        this.tail = this.tail.substring(index);
    }
    this.pos += match.length;
    return match;
  };

ok, 完成了Scanner的阅读我获取到几个有用信息

Scanner.prototype.scanUntil 会在遇到匹配标签时停下并返回前面那段字符串
Scanner.prototype.scan	会在遇到匹配标签时停下,跳过并且返回该匹配项

话不多说以图示意

好的接下来让我们阅读一下,这个while循环

value = scanner.scanUntil(openingTagRe)
 if (value) {
      for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
        chr = value.charAt(i);
        if (isWhitespace(chr)) {//空白符
          spaces.push(tokens.length);
          indentation += chr;
        } else {
          nonSpace = true;
          lineHasNonSpace = true;
          indentation += ' ';
        }
        tokens.push([ 'text', chr, start, start + 1 ]); //text用于标识该token的类型,区别view的需要
        start += 1;
        // Check for whitespace on the current line.
        // 有换行符的操作 ...
      }
    }
/*首先我们需要明确,在 "{{" 头标签之前停下的返回的match是不需要使用我们的view进行填充的,因此可以直接向tokens数组中push
这里需要这样一个逐个字符操作的原因是为了适配更多的场景比如<tag class="xx xx">,进行字符拆解作用可以便于之后stripSpace,
squashTokens这些函数的操作
*/
 if (!scanner.scan(openingTagRe)) // 跳过前标签 例如:{{,不存在前标签则退出while循环
      break;
 hasTag = true;
 // Get the tag type.
 type = scanner.scan(tagRe) || 'name'; //获取类似 {{#name}} 中的 #
 scanner.scan(whiteRe); // 跳过空白符
 // ......
if (type === '=') {
  value = scanner.scanUntil(equalsRe);
  scanner.scan(equalsRe);
  scanner.scanUntil(closingTagRe);
} else if (type === '{') { 
  value = scanner.scanUntil(closingCurlyRe);
  scanner.scan(curlyRe);
  scanner.scanUntil(closingTagRe); 
  type = '&';
} else { //默认的 {{ 关注部分
  value = scanner.scanUntil(closingTagRe); // 返回 {{#  name}}中的name部分
}
if (type == '>') {
    token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
} else {
token = [ type, value, start, scanner.pos ];
}

在这里我们需要确认一点 text 的 token 会被拆分为单个字符,但是 其他类型 如 name类型 #类型 都不会被被拆分为单个字符

3.2.2 squashTokens解读
//squash Tokens 字面理解对tokens进行压缩,前面我们也提到text 会被拆分那么这个时候我们需要重新在进行组装
function squashTokens (tokens) {
    var squashedTokens = [];
    var token, lastToken; //lastToken 用于拼接,引用的思想
    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
      token = tokens[i];
      if (token) {
        if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { //token 此处为合并的关键句
          lastToken[1] += token[1];
          lastToken[3] = token[3];
        } else {
          squashedTokens.push(token);
          lastToken = token; //保持对当前 token 的引用
        }
      }
    }
    return squashedTokens;
}
3.2.3 nestTokens解读:这里也是关键的一步
//nestTokens 字面理解 嵌套 tokens,在进行压缩后,使tokens变成一种token树的结构,为后面token tree --> htmlStr 做准备
function nestTokens (tokens) {//整体 使用 栈数据结构的思想 + 引用保存
    var nestedTokens = [];
    var collector = nestedTokens;
    var sections = [];
    var token, section;
    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
      token = tokens[i];
      switch (token[0]) {
        case '#':
        case '^':
          collector.push(token);
          sections.push(token); // 压栈
          collector = token[4] = []; // 更换collector的引用,token[4] 使为引用的token的4号索引位置创建一个空数组
          break;
        case '/':
          section = sections.pop(); // 弹栈
          section[5] = token[2];
          collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
          break;
        default:
          collector.push(token);
      }
    }
    return nestedTokens;
 }

我们需要获得一个如下结构的 token tree

tokenTree = [
    ['text', 'value'],
    ['#', 'property', childTree: [
     	['text', 'value'],
    	...
    ]],
    ['/', 'property'],
    ...
]

到此我们就完成了 template string -> token tree 的解析 就是 render必要操作的第一步

3.3 context 与 view

对于context,我们认为我们需要知道的是它与view 的关系

3.3.1首先是Context的属性
function Context (view, parentContext) { // Context 对应 view的管理 --> 也就是 data
    this.view = view;
    this.cache = { '.': this.view }; //默认 增加 { '.' : this.view }
    this.parent = parentContext; //通过 this.parent 引用 将整个 view 联系起来
 }
3.3.2 其次是其中的一个关键函数 lookup

这个函数也是有一定的篇幅,但是我们只需要我们所关注的

1.首先我们的得确定我们在应用中会出先 {{xxx.xxx}}的形式

2.其次mustache库中提供了一种下面的支持来更好的遍历 [1,2,3] 这种形式的数组

{{#list}}
	{{.}}
{{/list}}

3.我们会先使用当前 context的 属性值,但是当前不存在的时候我们不应该立即undefined,而是向上查找 this.parent

const view = {
    list: [{name: "xxx"}],
    apps: [1,2,3]
}
{{#list}}
	<h1>
    	{{#apps}}
        	<strong>{{name}}</strong> 
        {{/apps}}
    </h1>
{{/list}}
//输出结果 ===>
<h1>
    <strong>xxx</strong> 
 	<strong>xxx</strong> 
    <strong>xxx</strong> 
</h1>

3.4 最后的渲染 this.renderTokens
3.4.1 从简单的render 函数看起

​ 在通过对this.renderTokens函数的寻找过程中

Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {
    var buffer = '';
    var token, symbol, value;
    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
      value = undefined;
      token = tokens[i];
      symbol = token[0]; //获取token的 type
      if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);
      //... 中间这部分的判断并非我的关注点
      else if (symbol === 'name') value = this.escapedValue(token, context, config);
      else if (symbol === 'text') value = this.rawValue(token);
      if (value !== undefined)
        buffer += value;
    }
    return buffer;
};
/*
	其中 this.renderSection,this.escapedValue,this.rawValue(token); 应该是大同小异, 对text需要进行转义防止对页面插入一些脚本
*/
3.4.2 this.renderSection

​ 既然 this.renderSection,this.escapedValue,this.rawValue(token); 应该是大同小异,那么我们就取其中最为重要的 'this.renderSection’做文章,

​ 但是终究还是要用this.escapedValue,this.rawValue进行模板的填充

//配合上述的 this.renderTokens 形成递归,可以遍历整个token tree
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {
    var self = this;
    var buffer = '';
    var value = context.lookup(token[1]); // 取出view中 #property 对应的数据,用于判断
    // This function is used to render an arbitrary template
    // in the current context by higher-order sections.
    function subRender (template) {
      return self.render(template, context, partials, config);
    }
    if (!value) return;
    // 判断对应value 的 type 并且 创建一个新的 context
    if (isArray(value)) {
      for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
        buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);
      }
    } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
      buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);
    } else if (isFunction(value)) {
      // 错误处理
      value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
      if (value != null)
        buffer += value;
    } else {
      buffer += this.renderTokens(token[4], context, partials, originalTemplate, config);
    }
    return buffer;
  };

最终this.renderTokens 返回的 buffer就是我们调用 mustache.render(template., view)的结果

4. 结束总结

第一次分享个人的源码理解可能有许多需要努力的地方,如果你看到这里,那么感谢你的浏览,如有错误希望指出

下面是我学习mustache源码的方式

如有需要以下链接自取:

我是 加油 希望成为你的朋友

以下为个人blog地址,原创内容转载前请声明来源
https://sunboyzgz.github.io/2021/01/27/hello-mustache/

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值