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


    mustache.js(3.0.0版本) 是一个javascript前端模板引擎。官方文档(https://github.com/janl/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 }}}.

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

    |--- mustache 文件夹
    | |--- index.html
    | |--- mustache.js (库文件)

    基本代码如下所示:

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "<a>kongzhi<a>",
          "msg": {
            "sex": " male ", 
            "age": "31",
            "marriage": 'single'
          }
        }
        var tpl = '<p> {{name}}</p>'; 
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印如下:<p> &lt;a&gt;kongzhi&lt;a&gt;</p>
     </script>
    </body>
    </html>

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "<a>kongzhi<a>",
          "msg": {
            "sex": " male ", 
            "age": "31"
          }
        }
        var tpl = '<p> {{{name}}}</p>'; 
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 <p> <a>kongzhi<a></p>
     </script>
    </body>
    </html>

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

    2. 块

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

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

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "kongzhi",
          "msg": {
            "sex": " male ", 
            "age": "31",
            "marriage": 'single'
          }
        }
        var tpl = `{{ #msg }}<div>{{sex}}</div><div>{{age}}</div><div>{{marriage}}</div>{{ /msg }}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 <div> male </div><div>31</div><div>single</div>
     </script>
    </body>
    </html>

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

    Function

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "kongzhi",
          "msg": {
            "sex": " male ", 
            "age": "31",
            "marriage": 'single'
          },
          "wrapped": function() {
            return function(text, render) {
              return '<div>' + render(text) + '</div>'
            } 
          }
        }
        var tpl = `{{#wrapped}} {{name}} is men {{/wrapped}}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 <div> kongzhi is men </div>
     </script>
    </body>
    </html>

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "msg": [
            { 'firstName': 'kongzhi111', "lastName": 'kong' },
            { 'firstName': 'kongzhi222', "lastName": 'zhi' }
          ],
          "name": function() {
            return this.firstName + " " + this.lastName;
          }
        }
        var tpl = `{{#msg}} {{name}} {{/msg}}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 kongzhi111 kong  kongzhi222 zhi 
     </script>
    </body>
    </html>

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

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "kongzhi",
          "msg": null // 为null, undefined, '' 或 false,数据才会被渲染
        }
        var tpl = `{{ ^msg }}<div>暂无数据</div>{{ /msg }}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 <div>暂无数据</div>
     </script>
    </body>
    </html>

    2.3 {{.}}

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

    如下代码:

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "kongzhi",
          "msg": ['111', '222', '333']
        }
        var tpl = `{{#msg}} {{.}} * {{/msg}}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 111 *  222 *  333 * 
     </script>
    </body>
    </html>

    3. {{ !comments }}

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

    如下代码:

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": 'kongzhi'
        }
        var tpl = `<div>{{name}}</div>{{ ! 这是一段注释 }}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 <div>kongzhi</div> 
     </script>
    </body>
    </html>

    4. Partials的使用

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

    可以查看如下demo演示:

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        var data = {
          "name": "kongzhi",
          "msg": ['111']
        }
        var tpl = `{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}`;
        var html = Mustache.render(tpl, data); 
       console.log(html); // 打印 111 * <div>kongzhi</div>  
        /*
         * 如上我们的tpl模板文件中引入了 <div>{{name}}</div> 模块,但是该模块在其他的地方
         * 也使用到了,因此我们想让他当做一个模板定义,在需要的地方 引用进来。因此我们如下这样做了:
         var data = {
            "name": "kongzhi",
            "msg": ['111']
         }
         var temp = `<div>{{name}}</div>`;
         var tpl = `{{#msg}} {{.}} * {{> user }} {{/msg}}`;
         var html = Mustache.render(tpl, data, {
           user: temp
         }); 
         console.log(html); // 打印 111 * <div>kongzhi</div> 
        */ 
     </script>
    </body>
    </html>

    5. 设置分割符号

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

    <!DOCTYPE html>
    <html>
    <head>
      <title>mustache--demo</title>
      <meta charset="utf-8">
      <script type="text/javascript" src="./mustache.js"></script>
    </head>
    <body>
      <script type="text/javascript">
        console.log(Mustache);
        var data = {
          "name": "kongzhi",
          "msg": ['111']
        }
        var tpl = `{{%#msg%}} {{%.%}} * <div>{{%name%}}</div> {{%/msg%}}`;
        var html = Mustache.render(tpl, data, {}, [ '{{%', '%}}' ]); 
       console.log(html); // 打印 111 * <div>kongzhi</div>  
     </script>
    </body>
    </html>

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

    二:Mustache.js 源码分析

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

    {
      Context: fn(view, parentContext),
      Scanner: fn,
      Writer: fn,
      clearCache: fn,
      escape: function escapeHtml(){},
      name: "mustache.js",
      parse: fn(template, tags),
      render: fn(template, view, partials, tags),
      tags: ["{{", "}}"],
      to_html: fn(template, view, partials, send),
      version: "3.0.0"
    }

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

    1. 入口结构如下:

    (function defineMustache (global, factory) {
      /*
       如下判断支持 CommonJS 规范引入文件 或 AMD 规范引入文件,或直接引入js文件,
       Mustache 就是我们的全局变量对外暴露。
      */
      if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') {
        factory(exports); // CommonJS
      } else if (typeof define === 'function' && define.amd) {
        define(['exports'], factory); // AMD
      } else {
        global.Mustache = {};
        factory(global.Mustache); // script, wsh, asp
      }
    }(this, function mustacheFactory(mustache) {
      
      var objectToString = Object.prototype.toString;
      /*
       * 判断是否是一个数组的方法
      */
      var isArray = Array.isArray || function isArrayPolyfill (object) {
        return objectToString.call(object) === '[object Array]';
      };
      // 对象是否是一个函数
      function isFunction (object) {
        return typeof object === 'function';
      }
      // 判断类型
      function typeStr (obj) {
        return isArray(obj) ? 'array' : typeof obj;
      }
      function escapeRegExp (string) {
        return string.replace(/[-[]{}()*+?.,\^$|#s]/g, '\$&');
      }
      // 判断对象是否有该属性
      function hasProperty (obj, propName) {
        return obj != null && typeof obj === 'object' && (propName in obj);
      }
      // 判断原型上是否有该属性
      function primitiveHasOwnProperty (primitive, propName) {  
        return (
          primitive != null
          && typeof primitive !== 'object'
          && primitive.hasOwnProperty
          && primitive.hasOwnProperty(propName)
        );
      }
      // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
      // See https://github.com/janl/mustache.js/issues/189
      var regExpTest = RegExp.prototype.test;
      function testRegExp (re, string) {
        return regExpTest.call(re, string);
      }
    
      var nonSpaceRe = /S/;
      function isWhitespace (string) {
        return !testRegExp(nonSpaceRe, string);
      }
      // 对< > 等进行转义
      var entityMap = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
        '/': '&#x2F;',
        '`': '&#x60;',
        '=': '&#x3D;'
      };
      // 转换html标签进行转义操作
      function escapeHtml (string) {
        return String(string).replace(/[&<>"'`=/]/g, function fromEntityMap (s) {
          return entityMap[s];
        });
      }
    
      var whiteRe = /s*/;   // 匹配0个或多个空白
      var spaceRe = /s+/;   // 匹配至少1个或多个空白
      var equalsRe = /s*=/; // 匹配字符串 "=",且前面允许0个或多个空白符,比如 "=" 或 "  =" 这样的。
      var curlyRe = /s*}/; // 匹配 "}" 或 " }" 
      var tagRe = /#|^|/|>|{|&|=|!/; // 匹配 '#', '^' , '/' , '>' , { , & , = , ! 任何一个字符 
      // ...... 代码略
      mustache.name = 'mustache.js';
      mustache.version = '3.0.0';
      mustache.tags = [ '{{', '}}' ];
      // ..... 代码略
      mustache.escape = escapeHtml;
    
      // Export these mainly for testing, but also for advanced usage.
      mustache.Scanner = Scanner;
      mustache.Context = Context;
      mustache.Writer = Writer;
      mustache.clearCache = function clearCache () {};
      mustache.parse = function parse (template, tags) {};
      mustache.render = function render (template, view, partials, tags) {};
      mustache.to_html = function to_html (template, view, partials, send) {};
    }));

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

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

    var data = {
      "name": "kongzhi",
      "msg": ['111']
    }
    var tpl = `{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}`;
    var html = Mustache.render(tpl, data); 
    console.log(html); // 打印 111 * <div>kongzhi</div> 

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

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

    mustache.render = function render (template, view, partials, tags) {
      if (typeof template !== 'string') {
        throw new TypeError('Invalid template! Template should be a "string" ' +
                            'but "' + typeStr(template) + '" was given as the first ' +
                            'argument for mustache#render(template, view, partials)');
      }
    
      return defaultWriter.render(template, view, partials, tags);
    };

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

    var defaultWriter = new Writer();

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

    function Writer () {
      this.cache = {};
    }
    Writer.prototype.clearCache = function clearCache () {
      this.cache = {};
    };
    Writer.prototype.parse = function parse (template, tags) {
      // ...
    };
    Writer.prototype.render = function render (template, view, partials, tags) {
      // ...
    }
    Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) {
      // ...
    }
    Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
      // ...
    }
    Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
      // ...
    }
    Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
      // ...
    }
    Writer.prototype.unescapedValue = function unescapedValue (token, context) {
      // ...
    }
    Writer.prototype.escapedValue = function escapedValue (token, context) {
      // ...
    }
    Writer.prototype.rawValue = function rawValue (token) {
      // ...
    }

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

    /*
     @param {template} 值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";
     @param {view} 值为:{name: 'kongzhi', msg: ['111']}
    */
    Writer.prototype.render = function render (template, view, partials, tags) {
      var tokens = this.parse(template, tags);
      var context = (view instanceof Context) ? view : new Context(view);
      return this.renderTokens(tokens, context, partials, template);
    };

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

    /*
     @param {template} 值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";
     @param {tags} 值为:undefined
    */
    Writer.prototype.parse = function parse (template, tags) {
      var cache = this.cache;
      /*
       template = "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";
       tags的默认值:从源码可以看到:mustache.tags = [ '{{', '}}' ]; 因此:[ '{{', '}}' ].join(':') = "{{:}}"; 
       因此:cacheKey的值返回 "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}:{{:}}"
      */
      var cacheKey = template + ':' + (tags || mustache.tags).join(':');
    
      // 第一次 cache 为 {}; 所以 第一次 tokens 返回undefined; 
      var tokens = cache[cacheKey];
    
      /* 
        因此会进入 if语句内部,然后会调用 parseTemplate 模板进行解析,解析完成后,把结果返回 tokens = cache[cacheKey];
      */
      if (tokens == null)
        tokens = cache[cacheKey] = parseTemplate(template, tags);
      // 最后把token的值返回
      return tokens;
    };

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

    /*
     @param {template} 的值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";
     @param {tags} 的值为:undefined
    */
    function parseTemplate (template, tags) {
      // 没有模板,直接返回 [];
      if (!template)
        return [];
      var sections = [];     
      var tokens = [];       
      var spaces = [];       
      var hasTag = false;    
      var nonSpace = false;  
      // Strips all whitespace tokens array for the current line
      // if there was a {{#tag}} on it and otherwise only space.
      function stripSpace () {
        if (hasTag && !nonSpace) {
          while (spaces.length)
            delete tokens[spaces.pop()];
        } else {
          spaces = [];
        }
    
        hasTag = false;
        nonSpace = false;
      }
      var openingTagRe, closingTagRe, closingCurlyRe;
      function compileTags (tagsToCompile) {
        if (typeof tagsToCompile === 'string')
          tagsToCompile = tagsToCompile.split(spaceRe, 2);
    
        if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
          throw new Error('Invalid tags: ' + tagsToCompile);
    
        openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\s*');
        closingTagRe = new RegExp('\s*' + escapeRegExp(tagsToCompile[1]));
        closingCurlyRe = new RegExp('\s*' + escapeRegExp('}' + tagsToCompile[1]));
      }
      compileTags(tags || mustache.tags);
      var scanner = new Scanner(template);
      var start, type, value, chr, token, openSection;
      while (!scanner.eos()) {
        start = scanner.pos;
        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);
            } else {
              nonSpace = true;
            }
            tokens.push([ 'text', chr, start, start + 1 ]);
            start += 1;
            // Check for whitespace on the current line.
            if (chr === '
    ')
              stripSpace();
          }
        }
    
        // Match the opening tag.
        if (!scanner.scan(openingTagRe))
          break;
    
        hasTag = true;
    
        // Get the tag type.
        type = scanner.scan(tagRe) || 'name';
        scanner.scan(whiteRe);
    
        // Get the tag value.
        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);
        }
    
        // Match the closing tag.
        if (!scanner.scan(closingTagRe))
          throw new Error('Unclosed tag at ' + scanner.pos);
    
        token = [ type, value, start, scanner.pos ];
        tokens.push(token);
    
        if (type === '#' || type === '^') {
          sections.push(token);
        } else if (type === '/') {
          // Check section nesting.
          openSection = sections.pop();
    
          if (!openSection)
            throw new Error('Unopened section "' + value + '" at ' + start);
    
          if (openSection[1] !== value)
            throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
        } else if (type === 'name' || type === '{' || type === '&') {
          nonSpace = true;
        } else if (type === '=') {
          // Set the tags for the next time around.
          compileTags(value);
        }
      }
      // Make sure there are no open sections when we're done.
      openSection = sections.pop();
    
      if (openSection)
        throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
      return nestTokens(squashTokens(tokens));
    }

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

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

    Writer.prototype.parse = function parse (template, tags) {
      var cache = this.cache;
      var cacheKey = template + ':' + (tags || mustache.tags).join(':');
    } 

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

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

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

    function escapeRegExp (string) {
      // $& 的含义是:与 regexp 相匹配的子串。
      return string.replace(/[-[]{}()*+?.,\^$|#s]/g, '\$&');
    }

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

    return '{{'.replace(/[-[]{}()*+?.,\^$|#s]/g, '\$&'); 

    $& 含义是 与 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*}}/;

    继续往下执行代码:

    closingCurlyRe = new RegExp('\s*' + escapeRegExp('}' + tagsToCompile[1]));
    
    closingCurlyRe = new RegExp('\s*' + escapeRegExp('}' + "}}")) = /s*}}}/; 该closingCurlyRe是匹配 " }}}" 或 "}}}" 这样的。

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

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

    function Scanner (string) {
      this.string = string;
      this.tail = string;
      this.pos = 0;
    };

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

    this.string 值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"; 
    this.tail 的值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"; 
    this.pos = 0;
    因此 scanner 实例化的值为 = {
        string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
        tail: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
        pos: 0
      };

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

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

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

    Scanner.prototype.eos = function eos () {
      return this.tail === '';
    };

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

    1. 第一次while循环

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

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

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

    scanner 此时值为:= {
       pos: 0,
       string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
       tail: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    }

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

    Scanner.prototype.scan = function scan (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;
    };

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

    match = [
         "{{", 
         index: 0, 
         groups: undefined, 
         input: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    ];

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

    此时 scanner 的值为 = {
          pos: 2,
          tail: "#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    };

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

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

    var match = [
         "#",
         index: 0,
         groups: undefined,
         input: "#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    ];

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

    scanner = {
         pos: 3,
         tail: "msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
         string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    }

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

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

    scanner的值为:= {
        pos: 3,
        tail: "msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
         string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    };

    此时:this.tail 的值为 = "msg}} {{.}} * <div>{{name}}</div> {{/msg}}"; re = /s*/;

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

    Scanner.prototype.scan = function scan (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;
     };

    因此 match = "msg}} {{.}} * <div>{{name}}</div> {{/msg}}".match(/s*/);

    var match = [
            "",
            index: 0,
            group: undefined,
            input: "msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
     ];

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

    scanner = {
            pos: 3,
            tail: "msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
            string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
     };

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

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

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

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

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

    scanner = {
            pos: 3,
            string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
            tail: "msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
    };

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

    scanner = {
            pos: 6,
            string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
            tail: "}} {{.}} * <div>{{name}}</div> {{/msg}}"
     };

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

    scanner = {
             pos: 6,
             string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
             tail: "}} {{.}} * <div>{{name}}</div> {{/msg}}"
    };
    closingTagRe = "/s*}}/";

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

    Scanner.prototype.scan = function scan (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;
    };

    此时 this.tail 的值为 = "}} {{.}} * <div>{{name}}</div> {{/msg}}"; re = /s*}}/;
    var match = "}} {{.}} * <div>{{name}}</div> {{/msg}}".match(/s*}}/);

    var match = {
             "}}",
             groups: undefined,
             index: 0,
             input: "}} {{.}} * <div>{{name}}</div> {{/msg}}"
    };
    var string = match[0] = "}}";
    this.tail = this.tail.substring(string.length);

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

    scanner = {
             pos: 8,
             string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
             tail: " {{.}} * <div>{{name}}</div> {{/msg}}"
     };

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

    token = [ type, value, start, scanner.pos ];
    tokens.push(token);
    if (type === '#' || type === '^') {
        sections.push(token);
     } else if (type === '/') {
             // ...
     } else if (type === 'name' || type === '{' || type === '&') { 
          nonSpace = true;
     } else if (type === '=') {
          // Set the tags for the next time around.
          compileTags(value);
     }

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

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

    scanner = {
           pos: 8,
           string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
           tail: " {{.}} * <div>{{name}}</div> {{/msg}}"
     };

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

    value = scanner.scanUntil(openingTagRe); 
    scanUtil 源码函数如下(为了方便理解,继续贴下代码)
    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;
     };

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

    scanner = {
       pos: 9,
       string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
       tail: "{{.}} * <div>{{name}}</div> {{/msg}}"
     };

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

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

    var regExpTest = RegExp.prototype.test;
         function testRegExp (re, string) {
           return regExpTest.call(re, string);
         }
         var nonSpaceRe = /S/; // 匹配非空白字符
         function isWhitespace (string) {
           return !testRegExp(nonSpaceRe, string);
         }
         if (value) {
          for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
            chr = value.charAt(i);
    
            if (isWhitespace(chr)) {
              spaces.push(tokens.length);
            } else {
              nonSpace = true;
            }
    
            tokens.push([ 'text', chr, start, start + 1 ]);
            start += 1;
    
            // Check for whitespace on the current line.
            if (chr === '
    ')
              stripSpace();
          }
        }

    因此 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 === ' '; 则执行 stripSpace()方法,这里为false,因此不执行。
    继续执行如下代码:

    if (!scanner.scan(openingTagRe))
          break;
    openingTagRe的值为:openingTagRe = /{{s*/;

    scan 函数代码如下:

    Scanner.prototype.scan = function scan (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;
    };

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

    scanner = {
          pos: 9,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "{{.}} * <div>{{name}}</div> {{/msg}}"
    };

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

    var match = [
          "{{",
          index: 0,
          groups: undefined,
          input: "{{.}} * <div>{{name}}</div> {{/msg}}"
    ];
    var string = match[0] = "{{";
    this.tail = this.tail.substring(string.length);

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

    scanner = {
          pos: 11,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: ".}} * <div>{{name}}</div> {{/msg}}"
    };

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

    Scanner.prototype.scan = function scan (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;
    }

    由上面可知: 

    scanner = {
          pos: 11,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: ".}} * <div>{{name}}</div> {{/msg}}"
    };
    re = /#|^|/|>|{|&|=|!/;
    因此 var match = this.tail.match(re) = ".}} * <div>{{name}}</div> {{/msg}}".match(/#|^|/|>|{|&|=|!/);
    var match = [
          ">",
          index: 10,
          groups: undefined,
          input: ".}} * <div>{{name}}</div> {{/msg}}"
    ];

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

    var match = [
          '',
          groups: undefined,
          index: 0,
          input: ".}} * <div>{{name}}</div> {{/msg}}"
    ];

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

    scanner = {
          pos: 11,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: ".}} * <div>{{name}}</div> {{/msg}}"
     };

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

    if (type === '=') {
    
    } else if (type === '{') {
    
    } else {
          value = scanner.scanUntil(closingTagRe);
    }

    再来看下 scanUntil 代码如下:

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

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

    scanner = {
          pos: 12,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "}} * <div>{{name}}</div> {{/msg}}"
    };

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

    Scanner.prototype.scan = function scan (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;
     };

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

    var match = "}} * <div>{{name}}</div> {{/msg}}".match(/s*}}/);
        var match = [
          "}}",
          groups: undefined,
          index: 0,
          input: "}} * <div>{{name}}</div> {{/msg}}"
        ];
     var string = match[0] = "}}";
     this.tail = this.tail.substring(string.length);

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

    scanner = {
          pos: 14,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: " * <div>{{name}}</div> {{/msg}}"
     };

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

    因此此时 tokens = [
          ["#", "msg", 0, 8], 
          ['text', ' ', 8, 9],
          ["name", ".", 9, 14]
     ];

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

    第三次while循环

    此时scanner值为如下:

    scanner = {
        pos: 14,
        string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
        tail: " * <div>{{name}}</div> {{/msg}}"
      };

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

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

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

    scanner = {
        pos: 22,
        string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
        tail: "{{name}}</div> {{/msg}}"
      };

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

    if (value) {
        for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
          chr = value.charAt(i);
          if (isWhitespace(chr)) {
            spaces.push(tokens.length);
          } else {
            nonSpace = true;
          }
          tokens.push([ 'text', chr, start, start + 1 ]);
          start += 1;
          // Check for whitespace on the current line.
          if (chr === '
    ')
            stripSpace();
        }
      }
      var regExpTest = RegExp.prototype.test;
      function testRegExp (re, string) {
        return regExpTest.call(re, string);
      }
    
      var nonSpaceRe = /S/;
      function isWhitespace (string) {
        return !testRegExp(nonSpaceRe, string);
      }
    而此时 value.length = 8了;因此在for语句需要循环8次。
    
        i = 0:chr = value.charAt(i) = " * <div>".charAt(0) = " "; 执行 isWhitespace(chr); 函数代码,如下代码:
        RegExp.prototype.test.call(/S/, ' '); 判断 ' ' 是否是非空白字符,因此返回false,因此 !false 就是true了。
        因此 执行 spaces.push(tokens.length); 
        之前tokens = [["#", "msg", 0, 8],["text", " ", 8, 9],["name", ".", 9, 14]]; spaces = [1];
        因此此时 spaces = [1, 3]; 了。
        接着执行 tokens.push([ 'text', chr, start, start + 1 ]);
        因此 tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
        ];
        start += 1; 因此 start = 15;
    
        i = 1:chr = value.charAt(i) = " * <div>".charAt(1) = "*"; 执行 isWhitespace(chr); 返回false; 因此进入else
        语句; 此时:nonSpace = true; 继续执行代码:tokens.push([ 'text', chr, start, start + 1 ]); 因此tokens的值变为
        如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16]
        ];
        start += 1; 因此 start = 16;
    
        i = 2:chr = value.charAt(i) = " * <div>".charAt(2) = " "; 执行 isWhitespace(chr); 返回true; 和第一步一样,
        因此 执行 spaces.push(tokens.length); 因此 spaces.push(tokens.length); 即 spaces = [1, 3, 5];
        继续执行代码:tokens.push([ 'text', chr, start, start + 1 ]); 因此tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17]
        ];
        start +=1; 因此 start = 17;
    
        i = 3: chr = value.charAt(i) = " * <div>".charAt(3) = "<"; 执行 isWhitespace(chr); 返回false, 因此进入else
        语句。此时:nonSpace = true; 继续执行代码:tokens.push([ 'text', chr, start, start + 1 ]); 因此tokens的值变为
        如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18]
        ];
        start +=1; 因此 start = 18;
    
        i = 4: 同理,和第三步一样。因此 chr = 'd'; 因此tokens的值变为
        如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19]
        ];
        start +=1; 因此 start = 19;
    
        i = 5; 同理,和第三步一样。因此 chr = 'i'; 最后tokens的值变为:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20]
        ];
        start +=1; 因此 start = 20;
    
        i = 6; 同理,和第三步一样。因此 chr = 'v'; 最后tokens的值变为:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
        ];
        start +=1; 因此 start = 21;
    
        i = 7; 同理,和第三步一样。因此 chr = '>'; 最后tokens的值变为:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22]
        ];
        start +=1; 因此 start = 22;
    ng: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "{{name}}</div> {{/msg}}"
        }

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

    Scanner.prototype.scan = function scan (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;
        };

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

    scanner = {
          pos: 22,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "{{name}}</div> {{/msg}}"
        }

    因此 var match = "{{name}}</div> {{/msg}}".match(/{{s*/);

    var match = [
          "{{",
          index: 0,
          groups: undefined,
          input: "{{name}}</div> {{/msg}}"
        ];

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

    scanner = {
          pos: 24,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "name}}</div> {{/msg}}"
        };

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

    Scanner.prototype.scan = function scan (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;
        };

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

    var match = [
          '/',
          index: 7,
          groups: undefined,
          input: "name}}</div> {{/msg}}"
        ];

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

    var match = [
          "",
          index: 0,
          groups: undefined,
          input: "name}}</div> {{/msg}}"
        ];
        var string = match[0] = "";
        this.tail = this.tail.substring(string.length);
        this.tail = this.tail.substring(0);
        this.tail = "name}}</div> {{/msg}}";
        this.pos = 24;

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

    scanner = {
          pos: 24,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
          tail: "name}}</div> {{/msg}}"
        };

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

    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;
        };
        var closingTagRe = /s*}}/;

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

    scanner = {
          pos: 28,
          tail: "}}</div> {{/msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

    Scanner.prototype.scan = function scan (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;
        };

    因此值分别为如下:

    var match = "}}</div> {{/msg}}".match(/s*}}/);
        var match = [
          "}}",
          index: 0,
          groups: undefined,
          input: "}}</div> {{/msg}}"
        ];
        var string = match[0] = "}}";
        this.tail = this.tail.substring(string.length) = "}}</div> {{/msg}}".substring(2);
        this.tail = "</div> {{/msg}}";
        this.pos += string.length; this.pos = 28 + 2 = 30;

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

    scanner = {
          pos: 30,
          tail: "</div> {{/msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

    tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30]
        ];

    由于 type = "name"; 因此 nonSpace = true;

    第四次while循环

    此时 scanner = {
        pos: 30,
        string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}",
        tail: "</div> {{/msg}}"
      };
      start = scanner.pos; start = 30;
      // Match any text between tags.
      value = scanner.scanUntil(openingTagRe);
      openingTagRe 值为:openingTagRe = /{{s*/;

    调用 scanUntil 函数代码如下:

    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;
      };
      var index = "</div> {{/msg}}".search(/{{s*/);
      var index = 7;

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

    scanner = {
        pos: 37,
        tail: "{{/msg}}",
        string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
      };

    因此 value = "</div> ";
    因此会进入 if (value) {}; 语句代码,如下所示:

    if (value) {
        for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
          chr = value.charAt(i);
          if (isWhitespace(chr)) {
            spaces.push(tokens.length);
          } else {
            nonSpace = true;
          }
          tokens.push([ 'text', chr, start, start + 1 ]);
          start += 1;
          // Check for whitespace on the current line.
          if (chr === '
    ')
            stripSpace();
        }
      }

    由于value的值为 "</div> "; 长度为7. 因此会在内部for循环中循环7次。依次看下:

    1. i = 0; 
        执行 chr = value.charAt(i); 因此 chr = "<"; 此时 chr 不是空白字符,因此该 isWhitespace(chr) 函数返回false。
        因此 nonSpace = true; 接着执行:tokens.push([ 'text', chr, start, start + 1 ]); 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31]
        ];
        start += 1; 因此 start 值为 31.
    
        2. i = 1;
        同第一步一样,因此 chr = "/"; 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32]
        ];
        start += 1; 因此 start 值为 32.
    
        3. i = 2;
        同第一步一样,因此 chr = "d"; 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33]
        ];
        start += 1; 因此 start 值为 33.
    
        4. i = 3;
        同第一步一样,因此 chr = "i"; 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34]
        ];
        start += 1; 因此 start 值为 34.
    
        5. i = 4;
        同第一步一样,因此 chr = "i"; 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34],
          ["text", "v", 34, 35]
        ];
        start += 1; 因此 start 值为 35.
    
        6. i = 5;
        同第一步一样,因此 chr = ">"; 因此 tokens的值变为如下:
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34],
          ["text", "v", 34, 35],
          ["text", ">", 35, 36]
        ];
        start += 1; 因此 start 值为 36.
    
        7. i = 6
        执行 chr = value.charAt(i); 因此 chr = " "; 此时 chr 为空白字符,因此会执行 isWhitespace(chr) 函数返回true。
        因此执行 spaces.push(tokens.length); 因此 spaces 的值为:
        spaces = [1, 3, 5, 18]; 
        继续执行代码:tokens.push([ 'text', chr, start, start + 1 ]);
        因此
        tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34],
          ["text", "v", 34, 35],
          ["text", ">", 35, 36],
          ["text", " ", 36, 37]
        ];
        start += 1; 因此 start 值为37。

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

    Scanner.prototype.scan = function scan (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;
        };

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

    var match = [
          "{{",
          index: 0,
          groups: undefined,
          input: "{{/msg}}"
        ];
        var string = match[0] = "{{";
        this.tail = this.tail.substring(string.length);
        this.tail = "{{/msg}}".substring(2) = "/msg}}";
        this.pos += 2 = 39;

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

    scanner = {
          pos: 39,
          tail: "/msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

    var match = this.tail.match(re); var match = "/msg}}".match(/#|^|/|>|{|&|=|!/);
        var match = [
          "/",
          index: 0,
          groups: undefined,
          input: "/msg}}"
        ];
        var string = match[0] = "/";
        this.tail = this.tail.substring(string.length) = "/msg}}".substring(1);
        this.tail = "msg}}";
        this.pos += 1; 因此 this.pos = 40;

    最后返回 "/"; 因此 type = '/';
    此时 scanner 的值为如下:

    scanner = {
          pos: 40,
          tail: "msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

    var match = this.tail.match(re) = "msg}}".match(/s*/);
        var match = [
          "",
          index: 0,
          groups: undefined,
          input: "msg}}"
        ];
        var string = match[0]; var string = "";
        this.tail = this.tail.substring(string.length);
        this.tail = "/msg}}".substring(0) = "/msg}}";
        this.pos += string.length; this.pos = 40;

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

    此时 scanner = {
          pos: 40,
          tail: "msg}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

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

    因此 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 = {
          pos: 43,
          tail: "}}",
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
        };

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

    Scanner.prototype.scan = function scan (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;
        };

    因此 var match = "}}".match(/s*}}/);

    var match = [
          "}}",
          index: 0,
          groups: undefined,
          input: "}}"
        ];
        var string = match[0] = "}}";
        this.tail = this.tail.substring(string.length) = "}}".substring(2) = "";
        this.pos = 43 + 2 = 45;

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

    scanner = {
          pos: 45,
          string: "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}"
          tail: ""
        };

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

    tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34],
          ["text", "v", 34, 35],
          ["text", ">", 35, 36],
          ["text", " ", 36, 37],
          ['/', 'msg', 37, 45]
        ];

    此时此刻 type = '/'; 因此 会执行 

    else if (type === '/') {
          // Check section nesting.
          openSection = sections.pop();
          if (!openSection)
            throw new Error('Unopened section "' + value + '" at ' + start);
          if (openSection[1] !== value)
            throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);

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

    因此 这个时候会跳出while循环了,因为所有的字符都解析完毕了。最后一句代码:return nestTokens(squashTokens(tokens));

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

    function squashTokens (tokens) {
          var squashedTokens = [];
          var token, lastToken;
          for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            if (token) {
              if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
                lastToken[1] += token[1];
                lastToken[3] = token[3];
              } else {
                squashedTokens.push(token);
                lastToken = token;
              }
            }
          }
          return squashedTokens;
        }

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

    tokens = [
          ["#", "msg", 0, 8],
          ["text", " ", 8, 9],
          ["name", ".", 9, 14],
          ["text", " ", 14, 15],
          ["text", "*", 15, 16],
          ["text", " ", 16, 17],
          ["text", "<", 17, 18],
          ["text", "d", 18, 19],
          ["text", "i", 19, 20],
          ["text", "v", 20, 21],
          ["text", ">", 21, 22],
          ["name", "name", 22, 30],
          ["text", "<", 30, 31],
          ["text", "/", 31, 32],
          ["text", "d", 32, 33],
          ["text", "i", 33, 34],
          ["text", "v", 34, 35],
          ["text", ">", 35, 36],
          ["text", " ", 36, 37],
          ['/', 'msg', 37, 45]
        ];

    首先在函数内部定义一个新数组 var squashedTokens = []; 然后循环传进来的tokens的值。tokens的长度为20.
    为了更清楚的理解具体做了哪些事情,我们继续一步步把for循环拆开理解。如下所示:

    i = 0; 
          token = tokens[0] = ["#", "msg", 0, 8];
          因此进入 if (token) 内部代码,由于是第一次循环,所以 lastToken 为undefined,因此进入else语句代码;
          squashedTokens.push(token); 因此 squashedTokens = [ ["#", "msg", 0, 8] ];
          lastToken = token; 因此 lastToken = ["#", "msg", 0, 8];
    
          i = 1;
          token = tokens[1] = ["text", " ", 8, 9];
          进入if语句,if (token) 内部代码,这个时候 lastToken 有值了,token[0] === 'text' 为true,lastToken也为true,
          但是 lastToken = ["#", "msg", 0, 8]; 因此 lastToken[0] === 'text' 为false。因此还是进入else语句代码:
          因此 squashedTokens = [ ["#", "msg", 0, 8], ["text", " ", 8, 9] ]; lastToken = ["text", " ", 8, 9];
    
          i = 2;
          token = tokens[2] = ["name", ".", 9, 14];
          进入if语句代码,由于 token[0] === "name"; 因此进入else语句,此时 squashedTokens.push(token); 值为如下:
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14]
          ];
          lastToken = ["name", ".", 9, 14];
          ```
          i = 3;
          token = tokens[3] = ["text", " ", 14, 15];
          进入if(token)语句代码,token[0] === 'text' 为true; lastToken 也有值,为true,lastToken[0] === 'name' 为false,因此进入else语句代码, 因此 squashedTokens 和 lastToken 值分别为如下:
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " ", 14, 15]
          ];
          lastToken = ["text", " ", 14, 15];
          ```
          i = 4;
          token = tokens[4] = ["text", "*", 15, 16];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true,因此进入 第二个if
          语句代码:执行 lastToken[1] += token[1]; lastToken[3] = token[3]; 代码;
          即:lastToken[1] = " " + "*" = " *"; lastToken[3] = 16; 因此 lastToken = ["text", " *", 14, 16];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " *", 14, 16]
          ];
          ```
          这里明明没有进else内部代码,为什么这边 squashedTokens 对象值也发生变化呢?那是因为 代码里面对象数组的复制只是
          浅拷贝,如else内部代码:lastToken = token; 最后一次的token 和 lastToken 指向了同一个指针引用。因此也会导致数组
          squashedTokens 的某项的也会发生改变。
    
          i = 5;
          token = tokens[5] = ["text", " ", 16, 17];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true,
          因此 lastToken[1] += token[1]; lastToken[1] = " *" + " " = " * ";
          lastToken[3] = 17; 即:lastToken = ["text", " * ", 14, 17];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * ", 14, 17]
          ];
          ```
          i = 6;
          token = tokens[6] = ["text", "<", 17, 18];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true,
          因此 lastToken[1] += token[1]; lastToken[1] = " * " + "<" = " * <";
          lastToken[3] = 18; 即:lastToken = ["text", " * <", 14, 18];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <", 14, 18]
          ];
          ```
          i = 7;
          token = tokens[7] = ["text", "d", 18, 19];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true,
          因此 lastToken[1] += token[1]; lastToken[1] = " * <" + "d" = " * <d";
          lastToken[3] = 19; 即:lastToken = ["text", " * <d", 14, 19];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <d", 14, 19]
          ];
          ```
    
          i = 8;
          token = tokens[8] = ["text", "i", 19, 20];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; lastToken[1] = " * <d" + "i" = " * <di";
          lastToken[3] = 20; 即:lastToken = ["text", " * <di", 14, 20];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <di", 14, 20]
          ];
          ```
          i = 9;
          token = tokens[9] = ["text", "v", 20, 21];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; lastToken[1] = " * <di" + "v" = " * <div";
          lastToken[3] = 21; 即:lastToken = ["text", " * <div", 14, 21];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div", 14, 21]
          ]
          ```
    
          i = 10;
          token = tokens[10] = ["text", ">", 21, 22];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; lastToken[1] = " * <div" + ">" = " * <div>";
          lastToken[3] = 22; 即:lastToken = ["text", " * <div>", 14, 22];
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22]
          ]
          ```
          i = 11;
          token = tokens[11] = ["name", "name", 22, 30];
          token[0] === 'text' 为false, 因此进入else语句代码:squashedTokens.push(token); lastToken = token;
          ```
          因此 squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30]
          ];
          lastToken = ["name", "name", 22, 30];
          ```
          i = 12;
          token = tokens[12] = ["text", "<", 30, 31];
          由于上一步 lastToken[0] = "name"; 因此进入else语句代码,即:
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "<", 30, 31]
          ];
          lastToken = ["text", "<", 30, 31];
          ```
          i = 13;
          token = tokens[13] = ["text", "/", 31, 32];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此:lastToken[1] += token[1]; 即:lastToken[1] = "<" + "/" = "</";
          lastToken[3] = token[3]; 即:lastToken[3] = 32; 因此 lastToken = ["text", "</", 30, 32];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</", 30, 32]
          ];
          ```
          i = 14;
          token = tokens[14] = ["text", "d", 32, 33];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; 即:lastToken[1] = "</" + "d" = "</d";
          lastToken[3] = token[3]; 即:lastToken[3] = 33; 因此 lastToken = ["text", "</d", 30, 33];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</d", 30, 33]
          ];
          ```
          i = 15;
          token = tokens[15] = ["text", "i", 33, 34];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; 即:lastToken[1] = "</d" + "i" = "</di";
          lastToken[3] = token[3]; 即:lastToken[3] = 34; 因此 lastToken = ["text", "</di", 30, 34];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</di", 30, 34]
          ];
          ```
          i = 16;
          token = tokens[16] = ["text", "v", 34, 35];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; 即:lastToken[1] = "</di" + "v" = "</div";
          lastToken[3] = token[3]; 即:lastToken[3] = 35; 因此 lastToken = ["text", "</div", 30, 35];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</div", 30, 35]
          ];
          ```
          i = 17;
          token = tokens[17] = ["text", ">", 35, 36];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; 即:lastToken[1] = "</div" + ">" = "</div>";
          lastToken[3] = token[3]; 即:lastToken[3] = 36; 因此 lastToken = ["text", "</div>", 30, 36];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</div>", 30, 36]
          ];
          ```
          i = 18;
          token = tokens[18] = ["text", " ", 36, 37];
          token[0] === 'text' 为true; lastToken 也有值,为true, lastToken[0] === 'text' 也为true;
          因此 lastToken[1] += token[1]; 即:lastToken[1] = "</div>" + " " = "</div> ";
          lastToken[3] = token[3]; 即:lastToken[3] = 37; 因此 lastToken = ["text", "</div> ", 30, 37];
          ```
          squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</div> ", 30, 37]
          ];
          ```
          i = 19;
          token = tokens[19] = ['/', 'msg', 37, 45];
          token[0] === 'text' 为false,因此进入else语句代码。即:squashedTokens.push(token); lastToken = token;
          ```
          因此:squashedTokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</div> ", 30, 37]
            ['/', 'msg', 37, 45]
          ];
          lastToken = ['/', 'msg', 37, 45];

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

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

    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] = [];
                  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;
          }

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

    tokens = [
            ["#", "msg", 0, 8],
            ["text", " ", 8, 9],
            ["name", ".", 9, 14],
            ["text", " * <div>", 14, 22],
            ["name", "name", 22, 30],
            ["text", "</div> ", 30, 37],
            ['/', 'msg', 37, 45]
          ];

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

    i = 0;
    token = ["#", "msg", 0, 8];
    collector.push(token); 因此:collector = [ ["#", "msg", 0, 8] ];
    sections.push(token); 因此:sections =  [ ["#", "msg", 0, 8] ];
    执行:collector = token[4] = []; 
    因此 collector = []; sections = [ ["#", "msg", 0, 8, [] ] ];
    但是此时 nestedTokens = [ ["#", "msg", 0, 8, [] ] ];
    
    为什么是这样的呢?按道理来说 var nestedTokens = []; var collector = nestedTokens; 这两个数组是浅拷贝,但是为什么
    collector数组被置空了,为什么 nestedTokens 数组还是有数据呢?我们在理解之前,我们可以看如下demo来理解下:
    ```
    var arr1 = [];
    var arr2 = arr1;
    var token = [1];
    arr2.push(token);
    console.log(arr2); // [ [1] ]
    console.log(arr1); // [ [1] ]
    
    arr2 = token[1] = [];
    console.log(token); // [ [1], []]
    console.log(arr2); // []
    console.log(arr1); // [ [1], []]
    console.log(arr2.__proto__ === arr1.__proto__); // ture
    ```
    如上我们可以看到,上面两个demo是仿照我们代码的意思来的,其结果是一样的,虽然其中一个数组值为空了,但是另外一个数组并没有。
    那是因为 arr2 = token[1] = []; 这句代码,我们上面的token的值为 token = [1]; 这样的,然后设置 token[1] = []; 因此
    token = [1, []]; 最后把token[1] 的值赋值给 arr2了,因此arr2的值变为 []; 但是 arr1.__proto__ === arr2.__proto__
    是相等的,也就是他们俩还是指向了同一个引用,因为数组或对象 比较的不是值,而是是否是同一个引用。引用的还是同一个token对象。
    所以 arr2 = [], 但是 arr1 = [ [1], [] ]; 
    
    i = 1;
    token = ["text", " ", 8, 9];
    token[0] = "text", 因此进入default语句代码,因此 collector = [["text", " ", 8, 9]];
    由于 collector = token[4] = []; 是浅拷贝,因此 collector 值发生改变,会导致 token[4] 也会发生改变。
    因此 sections 值变为如下:
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9] ] ] ];
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9] ] ] ];
    
    i = 2;
    token = ["name", ".", 9, 14];
    token[0] = "name"; 因此 collector = [["text", " ", 8, 9],["name", ".", 9, 14]];
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14] ] ] ];
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14] ] ] ];
    i = 3;
    token = ["text", " * <div>", 14, 22];
    token[0] = "text"; 
    collector = [
      ["text", " ", 8, 9],
      ["name", ".", 9, 14],
      ["text", " * <div>", 14, 22]
    ];
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22] ] ] ];
    
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22] ] ] ];
    
    i = 4;
    token = ["name", "name", 22, 30];
    token[0] = "name";
    collector = [
      ["text", " ", 8, 9],
      ["name", ".", 9, 14],
      ["text", " * <div>", 14, 22],
      ["name", "name", 22, 30]
    ];
    
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30] ] ] ];
    
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30] ] ] ];
    
    i = 5;
    token = ["text", "</div> ", 30, 37];
    token[0] = "text";
    collector = [
      ["text", " ", 8, 9],
      ["name", ".", 9, 14],
      ["text", " * <div>", 14, 22],
      ["name", "name", 22, 30],
      ["text", "</div> ", 30, 37]
    ];
    
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] ] ];
    
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] ] ];
    
    i = 6;
    token = ['/', 'msg', 37, 45];
    token[0] = '/';
    
    此时此刻sections 和 nestedTokens 值如下:
    
    sections = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] ] ];
    
    nestedTokens = [ ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] ] ];
    
    因此会进入如下代码:
    
    case '/':
      section = sections.pop();
      section[5] = token[2];
      collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
      break;
    
    如上代码执行完成后;
    
    section = ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] ];
    sections = [];
    section[5] = token[2];
    
    因此 section 值变为如下:
    
    section = ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ];
    
    由于 sections.length = 0; 因此 把 nestedTokens 赋值给 collector; 
    collector = nestedTokens;
    
    最后返回的值:nestedTokens = [["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ]];
              

    因此 Writer.prototype.render 如下方法中的 第一个 this.parse() 函数就返回了值; 因此 tokens的值就是上面返回的值。

    Writer.prototype.render = function render (template, view, partials, tags) {
        var tokens = this.parse(template, tags);
        var context = (view instanceof Context) ? view : new Context(view);
        return this.renderTokens(tokens, context, partials, template);
      };

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

    Context 函数有如下方法:

    /*
         @param {view} { "name": "kongzhi", "msg": ['111'] }
         @param {parentContext} undefined
        */
        function Context (view, parentContext) {
          this.view = view;
          this.cache = { '.': this.view };
          this.parent = parentContext;
        }
        Context.prototype.push = function push (view) {
          return new Context(view, this);
        };
        Context.prototype.lookup = function lookup (name) {};

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

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

    tokens = [["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ]];
        context = {
          view: { "name": "kongzhi", "msg": ['111'] },
          cache: { '.' : { "name": "kongzhi", "msg": ['111'] } },
          parent: undefined,
          __proto__: {
            push: fn,
            lookup: fn
          }
        };
    partials = undefined;
    template = "{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";

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

    Writer.prototype.renderTokens = function renderTokens (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];
            symbol = token[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;
     }

    由上可知:tokens = [["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ]];

    因此循环tokens, 因此 token = tokens[i] = ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ];

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

    /*
           * @param {token} 
           token = ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ];
           * @param {context} 
            context = {
              view: { "name": "kongzhi", "msg": ['111'] },
              cache: { '.' : { "name": "kongzhi", "msg": ['111'] } },
              parent: undefined,
              __proto__: {
                push: fn,
                lookup: fn
              }
            };
           * @param {partials} 是否是可重用的模板,目前没有传递,值为undefined
           * @param {originalTemplate} 为页面模板 值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/msg}}";
          */
          Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
            var self = this;
            var buffer = '';
            var value = context.lookup(token[1]);
    
            // 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);
            }
            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);
              }
            } 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');
    
              // Extract the portion of the original template that the section contains.
              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;
          };

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

    Context.prototype.lookup = function lookup (name) {
            var cache = this.cache;
            var value;
            if (cache.hasOwnProperty(name)) {
              value = cache[name];
            } else {
              var context = this, intermediateValue, names, index, lookupHit = false;
              while (context) {
                if (name.indexOf('.') > 0) {
                  intermediateValue = context.view;
                  names = name.split('.');
                  index = 0;
                  while (intermediateValue != null && index < names.length) {
                    if (index === names.length - 1)
                      lookupHit = (
                        hasProperty(intermediateValue, names[index]) 
                        || primitiveHasOwnProperty(intermediateValue, names[index])
                      );
                    intermediateValue = intermediateValue[names[index++]];
                  }
                } else {
                  intermediateValue = context.view[name];
                  lookupHit = hasProperty(context.view, name);
                }
                if (lookupHit) {
                  value = intermediateValue;
                  break;
                }
                context = context.parent;
              }
              cache[name] = value;
            }
            if (isFunction(value))
              value = value.call(this.view);
            return value;
          };

    如上函数参数 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 = {
              view: { "name": "kongzhi", "msg": ['111'] },
              cache: { '.' : { "name": "kongzhi", "msg": ['111'] } },
              parent: undefined,
              __proto__: {
                push: fn,
                lookup: fn
              }
    };

    因此 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; 然后执行如下代码:

    if (lookupHit) {
             value = intermediateValue;
             break;
     }

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

    var data = {
             "name": "kongzhi",
             "msg": {
               "sex": " male ", 
               "age": "31",
               "marriage": 'single'
             },
             "wrapped": function() {
               return function(text, render) {
                 return '<div>' + render(text) + '</div>'
               } 
             }
    }
    var tpl = `{{#wrapped}} {{name}} is men {{/wrapped}}`;
    var html = Mustache.render(tpl, data); 
    console.log(html); // 打印 <div> kongzhi is men </div>

    代码: 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 函数代码可知:

    Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) {
              var self = this;
              var buffer = '';
              var value = context.lookup(token[1]);
    
              // 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);
              }
              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);
                }
              } 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');
    
                // Extract the portion of the original template that the section contains.
                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;
            }

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

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

    function Context (view, parentContext) {
         this.view = view;
         this.cache = { '.': this.view };
         this.parent = parentContext;
     }
     Context.prototype.push = function push (view) {
          return new Context(view, this);
      };

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

    context = {
         view: '111',
          cache: {".": "111"},
          parent: {
              parent: undefined,
              view: {
                 "name": "kongzhi",
                  "msg": ["111"]
               },
              cache: {
                "msg": ["111"],
                  ".": {
                    "name": "kongzhi",
                    "msg": ["111"]
                  }
                }
              }
     };

    我们之前的token值如下:

    token = ["#", "msg", 0, 8, [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ] , 37 ];


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

    Writer.prototype.renderTokens = function renderTokens (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];
                symbol = token[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[4] = [ ["text", " ", 8, 9], ["name", ".", 9, 14], ["text", " * <div>", 14, 22], ["name", "name", 22, 30], ["text", "</div> ", 30, 37] ];

    context 值就是我们上面的值;
    partials: 可重用的模板,目前没有传递给参数,因此为undefined。
    originalTemplate: 就是我们的模板,值为:"{{#msg}} {{.}} * <div>{{name}}</div> {{/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 = " ";

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

    /*
             @param {token} ["name", ".", 9, 14]
             @param {context}
             context = {
                view: '111',
                cache: {".": "111"},
                parent: {
                  parent: undefined,
                  view: {
                    "name": "kongzhi",
                    "msg": ["111"]
                  },
                  cache: {
                    "msg": ["111"],
                    ".": {
                      "name": "kongzhi",
                      "msg": ["111"]
                    }
                  }
                }
             };
            */
            Writer.prototype.escapedValue = function escapedValue (token, context) {
              var value = context.lookup(token[1]);
              if (value != null)
                return mustache.escape(value);
            };
            Context.prototype.lookup = function lookup (name) {
              var cache = this.cache;
              var value;
              if (cache.hasOwnProperty(name)) {
                value = cache[name];
              } else {
              }
              // ....
              return value;
            }

    如上代码可以看到,我们的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);
    };
    从源码当中我们知道:

    var entityMap = {
              '&': '&amp;',
              '<': '&lt;',
              '>': '&gt;',
              '"': '&quot;',
              "'": '&#39;',
              '/': '&#x2F;',
              '`': '&#x60;',
              '=': '&#x3D;'
            };
            mustache.escape = escapeHtml;
            function escapeHtml (string) {
              return String(string).replace(/[&<>"'`=/]/g, function fromEntityMap (s) {
                return entityMap[s];
              });
            }

    最后就会返回一个字符串,从该代码中,我们也可以看到,如果模板中有 <a> 这样类似的标签的时候,它会转换成 "&lt;a&gt;" 这样
    的,会对html标签进行转义操作。因此这里我们的值 value 就返回字符串 "111" 了。 buffer = " " + "111" 因此 最后 buffer = " 111";

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

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

    /*
             @param {token} ["name", "name", 22, 30]
             @param {context}
             context = {
                view: '111',
                cache: {".": "111"},
                parent: {
                  parent: undefined,
                  view: {
                    "name": "kongzhi",
                    "msg": ["111"]
                  },
                  cache: {
                    "msg": ["111"],
                    ".": {
                      "name": "kongzhi",
                      "msg": ["111"]
                    }
                  }
                }
             };
            */
            Writer.prototype.escapedValue = function escapedValue (token, context) {
              var value = context.lookup(token[1]);
              if (value != null)
                return mustache.escape(value);
            };
            Context.prototype.lookup = function lookup (name) {
              var cache = this.cache;
              var value;
              if (cache.hasOwnProperty(name)) {
                value = cache[name];
              } else {
                var context = this, intermediateValue, names, index, lookupHit = false;
                while (context) {
                  if (name.indexOf('.') > 0) {
                    // ... 代码省略
                  } else {
                    intermediateValue = context.view[name];
                    lookupHit = hasProperty(context.view, name);
                  }
                  if (lookupHit) {
                    value = intermediateValue;
                    break;
                  }
                  context = context.parent;
                }
                cache[name] = value;
              }
              // ....
              return value;
            }

    如上代码,执行 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 = {
              view: '111',
              cache: {".": "111"},
              parent: {
                parent: undefined,
                view: {
                  "name": "kongzhi",
                  "msg": ["111"]
                },
                cache: {
                  "msg": ["111"],
                  ".": {
                    "name": "kongzhi",
                    "msg": ["111"]
                  }
                }
              }
     };

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

    context = {
              parent: undefined,
              view: {
                "name": "kongzhi",
                "msg": ["111"]
              },
              cache: {
                "msg": ["111"],
                ".": {
                  "name": "kongzhi",
                  "msg": ["111"]
                }
              }
            }

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

    if (lookupHit) {
          value = intermediateValue;
          break;
     }

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

    cache = {
              ".": 111,
              "name": "kongzhi"
            }

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

    context = {
              view: '111',
              cache: {".": "111", "name": "kongzhi"},
              parent: {
                parent: undefined,
                view: {
                  "name": "kongzhi",
                  "msg": ["111"]
                },
                cache: {
                  "msg": ["111"],
                  ".": {
                    "name": "kongzhi",
                    "msg": ["111"]
                  }
                }
              }
    };

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

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

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

    Writer.prototype.escapedValue = function escapedValue (token, context) {
         var value = context.lookup(token[1]);
         if (value != null)
              return mustache.escape(value);
     };

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

    Writer.prototype.renderTokens = function renderTokens (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];
                symbol = token[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;
     }

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

    5. 第五个数组为:token = ["text", "</div> ", 30, 37]; 由于 token[0] = "text"; 因此:value = this.rawValue(["text", " * <div>", 14, 22]); rawValue 函数代码如下:

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

    buffer = " 111 * <div>kongzhi</div> ";

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

    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);

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

    Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) {
         var value = context.lookup(token[1]);
         // Use JavaScript's definition of falsy. Include empty arrays.
         // See https://github.com/janl/mustache.js/issues/186
         if (!value || (isArray(value) && value.length === 0))
              return this.renderTokens(token[4], context, partials, originalTemplate);
     };
    
     Writer.prototype.renderPartial = function renderPartial (token, context, partials) {
          if (!partials) return;
          var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
           if (value != null)
                return this.renderTokens(this.parse(value), context, partials, value);
     };
    
    Writer.prototype.unescapedValue = function unescapedValue (token, context) {
         var value = context.lookup(token[1]);
         if (value != null)
            return value;
     };

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

  • 相关阅读:
    类型转换
    Java中this和super的用法总结
    关于网页乱码问题
    用cookie实现记住用户名和密码
    Before start of result set
    jsp页面错误The attribute prefix does not correspond to any imported tag library
    MySql第几行到第几行语句
    servelet跳转页面的路径中一直包含sevelet的解决办法
    <a>标签跳转到Servelet页面并实现参数的传递
    解决网页乱码
  • 原文地址:https://www.cnblogs.com/tugenhua0707/p/11762948.html
Copyright © 2020-2023  润新知