mustache-2.0源码注解

源码:https://cdn.bootcdn.net/ajax/libs/mustache.js/2.0.0/mustache.js

由于2.0与1.0差别不是很大,所有我就只单独比较一下 区别

1. Context 类

2.0这边不做view数据是否是null的判断了。直接赋值

function Context(view, parentContext) {
    this.view = view;  // 1.0 this.view = view == null ? {} : view;
    this.cache = { '.': this.view };
    this.parent = parentContext;
  }

2. lookup方法

lookup方法增加了一个标识lookupHit(是否查找命中).而1.0是判断是否有获取到value的值来判断

Context.prototype.lookup = function (name) {
    var cache = this.cache;

    var value;
    if (name in cache) {
      value = cache[name];
    } else {
      // 增加一个标识 lookupHit 是否查找命中 
      var context = this, names, index, lookupHit = false;

      while (context) {
        if (name.indexOf('.') > 0) {
          value = context.view;
          names = name.split('.');
          index = 0;

     
          while (value != null && index < names.length) {
            if (index === names.length - 1 && value != null)
            // 对象自身属性中是否具有指定的属性(也就是,是否有指定的键), 那么查找到了。循环结束
              lookupHit = (typeof value === 'object') &&
                value.hasOwnProperty(names[index]);
            value = value[names[index++]];
          }
        } else if (context.view != null && typeof context.view === 'object') {
          value = context.view[name];
          // 对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
          lookupHit = context.view.hasOwnProperty(name);
        }
        // 命中查找 结束循环 (1.0则是value有值就结束循环)
        if (lookupHit)
          break;

        context = context.parent;
      }

      cache[name] = value;
    }

    if (isFunction(value))
      value = value.call(this.view);

    return value;
  };

3. renderTokens

对应renderTokens方法里面其实改变不多, 更多的是把每种情况给单独用一个独立的函数处理,而不是像1.0一样都在 renderTokens 中处理,看起来更简洁易懂了。


  Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
    var buffer = '';
    // 
    var token, symbol, value;
    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
      value = undefined;
      token = tokens[i];
      // 现在用symblo临时保存 token[0], 1.0直接判断token[0]
      symbol = token[0];
      // 1.0是用 的switch(token[0]) 现在改为 if else 
      // 并且每一项symbol都用一个函数处理,而不是1.0直接在这个函数中处理
      if (symbol === '#') value = this._renderSection(token, context, partials, originalTemplate);
      else if (symbol === '^') value = this._renderInverted(token, context, partials, originalTemplate);
      else if (symbol === '>') value = this._renderPartial(token, context, partials, originalTemplate);
      else if (symbol === '&') value = this._unescapedValue(token, context);
      else if (symbol === 'name') value = this._escapedValue(token, context);
      else if (symbol === 'text') value = this._rawValue(token);

      if (value !== undefined)
        buffer += value;
    }

    return buffer;
  };

4. renderTokens里面对应 token[0] === '#'的情况

2.0比1.0 在判断条件中 增加了数字类型的情况

  Writer.prototype._renderSection = function (token, context, partials, originalTemplate) {
    var self = this;
    var buffer = '';
    var value = context.lookup(token[1]);


    function subRender(template) {
      return self.render(template, context, partials);
    }

    if (!value) return;

    if (isArray(value)) {
      for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
        buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate);
      }
      // 比1.0 多了数字类型  
    } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
      buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate);
    } else if (isFunction(value)) {
      if (typeof originalTemplate !== 'string')
        throw new Error('Cannot use higher-order sections without the original template');

    
      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);
    }
    return buffer;
  };

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="./mustache2.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}}

      {{#arr1}}
          <p>{{.}}</p>
      {{/arr1}}

      {{#arr2}}
          <p>{{top}}</p>
      {{/arr2}}

      {{#arr3}}
          <p>{{99}}</p>
      {{/arr3}}

  

      <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)
        },
        arr1: ['第一个','第二个'],
        arr2: [{top:'test1'},{top: 123}],
        arr3: [{99:'test1'},{99: 123}],
      };
      const str = Mustache.render(template, data, {abc: '123'});
      // const str = Mustache.render(template, data, (a)=>{
      //   return a + '333'
      // });
      console.log('str', str);
      document.querySelector('#app').innerHTML=str
    </script>
  </body>
</html>

源码:


/*!
 * 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) {

   /*** 判断是否是数组 -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, "\\$&");
  }

  // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
  // See https://github.com/janl/mustache.js/issues/189
  /**** 正则 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  *****/ 

  /*** escape start ****/
  var entityMap = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': '&quot;',
    "'": '&#39;',
    "/": '&#x2F;'
  };

  function escapeHtml(string) {
    return String(string).replace(/[&<>"'\/]/g, function (s) {
      return entityMap[s];
    });
  }
  /*** escape end ****/

  var whiteRe = /\s*/; // 空格可能存在
  var spaceRe = /\s+/; // 最少有一个空格
  var equalsRe = /\s*=/; // 等于号前面可能存在空格
  var curlyRe = /\s*\}/; // 等于号前面最少存在一个空格
  var tagRe = /#|\^|\/|>|\{|&|=|!/; // 最少存在匹配里面一个符号

  /**
   *  处理模板为tokens
   */
  function parseTemplate(template, tags) {
    // 如果没有传入模板,直接返回空数组
    if (!template)
      return [];

    var sections = []; // 栈(先进后出,后进先出)
    var tokens = []; // 获取的tokens  (会切割得很细,一个空格也会是一个token)
    var spaces = []; // 这个数据会保存 tokens 中 保存的是空格 的位置
    var hasTag = false; // 当前行上有{{tag}}吗?
    var nonSpace = false; // 当前行中是否有非空格字符?

    // 处理空格 此时 每一项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.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) {
    this.view = 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 初始定义为 当前对象,下面可能会改变
      // 增加一个标识 lookupHit 是否查找命中 
      var context = this, names, index, lookupHit = false;
      // 循环遍历 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) {
            if (index === names.length - 1 && value != null)
            // 对象自身属性中是否具有指定的属性(也就是,是否有指定的键), 那么查找到了。循环结束
              lookupHit = (typeof value === 'object') &&
                value.hasOwnProperty(names[index]);
            value = value[names[index++]];
          }
        } 
        // 如果 查找的关键字没有点  并且 数据源是对象。那么直接返回匹配的值
        else if (context.view != null && typeof context.view === 'object') {
          value = context.view[name];
          // 对象自身属性中是否具有指定的属性(也就是,是否有指定的键)
          lookupHit = context.view.hasOwnProperty(name);
        }
        // 命中查找 结束循环 (1.0则是value有值就结束循环)
        if (lookupHit)
          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) {
    // 初始为需要的dom字符串
    var buffer = '';

    var token, symbol, value;
    for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
      value = undefined;
      token = tokens[i];
      // 现在用symblo临时保存 token[0], 1.0直接判断token[0]
      symbol = token[0];

      // 1.0是用 的switch(token[0]) 现在改为 if else 
      // 并且每一项symbol都用一个函数处理,而不是1.0直接在这个函数中处理
      if (symbol === '#') value = this._renderSection(token, context, partials, originalTemplate);
      else if (symbol === '^') value = this._renderInverted(token, context, partials, originalTemplate);
      else if (symbol === '>') value = this._renderPartial(token, context, partials, originalTemplate);
      else if (symbol === '&') value = this._unescapedValue(token, context);
      else if (symbol === 'name') value = this._escapedValue(token, context);
      else if (symbol === 'text') value = this._rawValue(token);

      if (value !== undefined)
        buffer += value;
    }

    return buffer;
  };

  // 如果是# 那这个token 就是循环的token 
  // 比如 ["#", "list", 194, 203, Array(5), 260]
  Writer.prototype._renderSection = function (token, context, partials, originalTemplate) {
    var self = this;
    var buffer = '';
    // 数据中查找 list 对应的 数据
    var value = context.lookup(token[1]);

    // 附属渲染  获取  顶层渲染的数据
    function subRender(template) {
      return self.render(template, context, partials);
    }
    // 如果数据没有则跳过
    if (!value) return;

    // 如果是 数组
    // 比如  {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);
      }
      // 比1.0 多了数字类型
      //如果 找到的 value 为对象或字符串
      // {list: {name:'zs', age:14}} 或者 {list: 'abcd'} 或者 {list: 1234}
      // value 为 {name:'zs', age:14} 或 'abcd' 或1234
    } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
      // 也是递归 把 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);
    }
    return buffer;
  };

  Writer.prototype._renderInverted = function(token, context, partials, originalTemplate) {
    // 查找到 数据中的对应值 (lookup方法会找到最上层)
    var value = context.lookup(token[1]);

    // 如果没有找到值 或者 是一个空数组  调用 renderTokens 方法查找对应数据
    if (!value || (isArray(value) && value.length === 0))
      return this.renderTokens(token[4], context, partials, originalTemplate);
  };

  Writer.prototype._renderPartial = function(token, context, partials) {
    // > 如果没有 分部 partials 跳过
    if (!partials) return;
    // 判断partials 是不是函数,函数执行 否则世界查找partials的 值
    var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
    if (value != null)
    // 有值的话继续递归
      return this.renderTokens(this.parse(value), context, partials, value);
  };

  // 如果是& 说明是{{{}}} 中的数据, 去context 中查找到对应的数据 后直接拼接
  Writer.prototype._unescapedValue = function(token, context) {
    var value = context.lookup(token[1]);
    if (value != null)
      return value;
  };

  // name  去context 中查找到对应的数据 
  Writer.prototype._escapedValue = function(token, context) {
    var value = context.lookup(token[1]);
    // 如果不为空,转义之后拼接
    if (value != null)
      return mustache.escape(value);
  };

  // text 是文本 ,直接拼接
  Writer.prototype._rawValue = function(token) {
    return token[1];
  };

  mustache.name = "mustache.js";
  mustache.version = "2.0.0";
  mustache.tags = [ "{{", "}}" ];

  // 实例化  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);
  };

  // 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;
    }
  };

  // escape 作用是把特殊符号转移
  mustache.escape = escapeHtml;

  // 导出三个类主要用于测试,但也用于高级用途
  mustache.Scanner = Scanner;
  mustache.Context = Context;
  mustache.Writer = Writer;

}));

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容