1. 程式人生 > >vue系列---Mustache.js模板引擎介紹及原始碼解析(十)

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 === '\n')
          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 === '\n')
          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 === '\n'; 則執行 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 === '\n')
        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] = "}}";