mustache-1.0源碼注解

源碼地址:https://cdn.bootcdn.net/ajax/libs/mustache.js/1.0.0/mustache.js

大概的方法

{{name}} 會(huì)把數(shù)據(jù)中 name 轉(zhuǎn)換為具體值
{{#list}} {{/list}} 會(huì)把list中的數(shù)據(jù)選項(xiàng)展示
{{^list}}{{/list}} 數(shù)據(jù)中 list不存在或者未空數(shù)組才展示
{{!注釋}} 注釋
{{{}}} 里面的內(nèi)容不會(huì)轉(zhuǎn)義
{{>name}} 會(huì)從附屬數(shù)據(jù)中獲取數(shù)據(jù)
<div>{{=<% %>=}}</div> 修改 <% %> 為新切割符號(hào)

html測(cè)試

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>

    <script src="./mustache1.0.js"></script>
    <script>
      var template = `
      <div>{{name}}</div>
      
      <div>{{age}}</div>

      <div>{{!我是注釋}}</div> 
      <div>{{{f1}}}</div> 
      <div>{{{f2}}}</div> 
      
      
      <div>{{<template2}}</div>

      {{^list}}
        <span>姓名:{{name}}<i>年齡:{{age}}</i></span>
      {{/list}}

      {{#arr}}
        <span>{{test}}</span>
      {{/arr}}

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

      <div>{{>abc}}</div>
      <div>{{=<% %>=}}</div>
      <div><%name%></div>
      `;
      var template2 = '<sapn>{{sex}}</sapn';
      var data = {
        name: '張三',
        age: 14,
        f1: 'abc&<',
        f2: function () {
          return 666;
        },
        f3: 'abc&<',
        sex: '男',
        test: 'tttttttt',
        list: [], //也可以不要這一項(xiàng)
        arr: ()=> (template, fn)=>{
          return fn(template)
        },
        arr2: [{top:'test1'},{top:'test2'}],
      };
      const str = Mustache.render(template, data, {abc: '123'});
      // const str = Mustache.render(template, data, (a)=>{
      //   return a + '333'
      // });
      console.log('str', str);
    </script>
  </body>
</html>

image.png

原理

1.0是 大概原理是 把template模板 通過(guò) 對(duì)應(yīng)的分隔符號(hào) 比如 {{ }} 轉(zhuǎn)為為tokens;

["text", "</div>?", 181, 188]
["^", "list", 194, 203, Array(5), 260]
["text", " <span>姓名:", 204, 221]
["name", "name", 221, 229]
["text", "<i>年齡:", 229, 235]
["name", "age", 235, 242]
["text", "</i></span>?", 242, 254]
["/", "list", 260, 269]
["#", "arr", 276, 284, Array(3), 321]

[type, value, start, scanner.pos]

  • 其中type 是這個(gè) 類(lèi)型 text 為文本, name需要取數(shù)據(jù)中的鍵 ,# 是循環(huán)
  • value 數(shù)據(jù)中的鍵或者字符串
  • start 這個(gè)是當(dāng)前數(shù)組所在字符串的位置
  • scanner.pos 掃描器的位置或者未結(jié)束位置

然后把tokens處理為 dom識(shí)別的字符串;

源碼里面有三個(gè)類(lèi):

  • Scanner;
  • Context;
  • Writer;

Scanner 掃描器

我的理解 Scanner 是 當(dāng) 匹配符號(hào)比如 {{ 和 }} 匹配一段文本是分別 獲取 匹配符號(hào)之外的文本。(其實(shí)跟以前版本正則類(lèi)似)
比例: 我是{{name}},我愛(ài)我的{{contry}}。也愛(ài)你
通過(guò)Scanner類(lèi)可以獲取到 五段內(nèi)容:

  1. 我是
  2. name
  3. ,我愛(ài)我的
  4. contry
  5. 。也愛(ài)你

源碼


    /**
     * 模板解析器用于在模板字符串中查找令牌的簡(jiǎn)單字符串掃描儀
     * 獲取字符串,并設(shè)置尾部,尾巴會(huì)向后減少。pos是當(dāng)前位置
     */
    function Scanner(string) {
        this.string = string;
        this.tail = string;
        this.pos = 0;
    }
    /**
     * 判斷是否到尾部
     */
    Scanner.prototype.eos = function () {
        return this.tail === "";
    };
    /**
     * 為了跳過(guò)匹配符號(hào)
     * 返回匹配的匹配的符號(hào),如果沒(méi)有返回空字符串
     * 比如 re為 {{   找到的話就返回 {{  并且尾巴向后移動(dòng)兩位,pos位置也加2
     */
    Scanner.prototype.scan = function (re) {
        var match = this.tail.match(re);
        // 沒(méi)有匹配直接返回空字符串
        if (!match || match.index !== 0)
            return '';
        var string = match[0];
        // 尾巴 向后移動(dòng) 匹配符號(hào)的長(zhǎng)度
        this.tail = this.tail.substring(string.length);
        // 位置加 上匹配符號(hào)的長(zhǎng)度
        this.pos += string.length;
        return string;
    };
    /**
     * 為了找到 匹配符號(hào)前的文本
     * 如果有匹配的符號(hào),返回匹配符號(hào)之前的文本,否則就是沒(méi)有匹配到,返回剩下的尾巴
     * 比如 這里是{{name}}的家  re為 {{  會(huì)返回 這里是
     */
    Scanner.prototype.scanUntil = function (re) {
        // 得到匹配符號(hào)在尾巴中的位置(尾巴會(huì)變化,所有位置也會(huì)在變化)
        var index = this.tail.search(re),
            match;
        switch (index) {
            // -1說(shuō)明沒(méi)有匹配到特殊符號(hào),說(shuō)明已經(jīng)掃描完了。直接返回剩下的文本。比把尾巴置空
            case -1:
                match = this.tail;
                this.tail = "";
                break;
            // 特殊符號(hào)剛好在開(kāi)始地方,那匹配符號(hào)前 就沒(méi)有文本。匹配到的就是空字符串
            case 0:
                match = "";
                break;
            // 其他情況 直接獲取匹配符號(hào)前的文本,并把尾巴向后移動(dòng)到匹配的地方(這個(gè)時(shí)候后面會(huì)用scan函數(shù)跳過(guò)匹配符號(hào))
            default:
                match = this.tail.substring(0, index);
                this.tail = this.tail.substring(index);
        }
        // 位置 需要加上匹配的長(zhǎng)度 ,并返回匹配的文本 
        this.pos += match.length;
        return match;
    };

html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app"></div>

    <script src="./mustache1.0.js"></script>
    <script>
      var template = `
      <div>{{name}}</div>
      <div>{{age}}</div>
      <div>{{!我是注釋}}</div> 
      <div>{{{f1}}}</div> 
      <div>{{{f2}}}</div> 
      <div>{{f3}}</div>
      <div>{{<template2}}</div>
      {{#list}}
        <span>姓名:{{name}}<i>年齡:{{age}}</i></span>
      {{/list}}
      `;
     
   
      var scaner = new Mustache.Scanner(template)
      console.log('scaner', scaner);
      console.log('scanUntil', scaner.scanUntil("{{"));
      console.log('scan', scaner.scan("{{"));
      console.log('scanUntil', scaner.scanUntil("}}"));
      console.log('scan', scaner.scan("}}"));
    </script>
  </body>
</html>

由于這個(gè)類(lèi)向外暴露了。我們可以直接測(cè)試


image.png

這里我只看一下效果,后面代碼里面會(huì)循環(huán)調(diào)用;

Context 上下文

作用是用來(lái)處理data數(shù)據(jù),便于查找對(duì)應(yīng)的數(shù)據(jù)

/**
     * 
     * view 類(lèi) 數(shù)據(jù) 比如 {{name: 'zs', age: 14}}
     * 把數(shù)據(jù)作為參數(shù) 實(shí)例化Context 。便于 創(chuàng)建新的數(shù)據(jù) 并且根據(jù)樹(shù)結(jié)構(gòu)查找數(shù)據(jù)
     */
    function Context(view, parentContext) {
        // view 如果不存在,默認(rèn)設(shè)置為空對(duì)象
        this.view = view == null ? {} : view;
        // 緩存  對(duì)象  .
        this.cache = {
            '.': this.view
        };
        // 設(shè)置父元素為 傳入的第二個(gè)參數(shù)
        this.parent = parentContext;
    }
    /**
     * 使用給定視圖創(chuàng)建一個(gè)新上下文,并將此上下文作為父級(jí)。
     */
    Context.prototype.push = function (view) {
        return new Context(view, this);
    };
    /**
     * 返回此上下文中給定名稱的值,如果此上下文視圖中缺少該值,則遍歷上下文層次結(jié)構(gòu)。
     */
    Context.prototype.lookup = function (name) {
        // 首先拿到緩存
        var cache = this.cache;
        // 定義查找的內(nèi)容
        var value;
        // 如果要查找的 內(nèi)容在緩存中  賦值為value
        // 如果是循環(huán) 的情況,獲取 .  這個(gè)符號(hào),會(huì)直接返回 上面 有緩存對(duì)象
        if (name in cache) {
            value = cache[name];
        } else {
            // context 初始定義為 當(dāng)前對(duì)象,下面可能會(huì)改變
            var context = this,
                names, index;
            // 循環(huán)遍歷 context ,知道context不存在為止
            while (context) {
                // 如果要查找的是 a.b.c 類(lèi)似 的數(shù)據(jù)
                if (name.indexOf('.') > 0) {
                    // 首先把 數(shù)據(jù)賦值為value  比如 { a: { b: c: 'haha' } }
                    value = context.view;
                    // names 把 a.b.c 拆分為 [a.b.c]
                    names = name.split('.');
                    // index 初始為0
                    index = 0;
                    //  循環(huán) names 數(shù)組 ,并分別從value中查找到值,并重新賦值為value
                    //  比如上面最后會(huì)得到 value 為 haha
                    while (value != null && index < names.length)
                        value = value[names[index++]];

                } 
                // 如果 查找的關(guān)鍵字沒(méi)有點(diǎn)  并且 數(shù)據(jù)源是對(duì)象。那么直接返回匹配的值
                else if (typeof context.view == 'object') {
                    value = context.view[name];
                }
                // 如果找到匹配值,那么終止循環(huán)
                if (value != null)
                    break;
                // 如果沒(méi)有找到匹配的值,那么把context 上下文重新定義為 父 上下文,并重新循環(huán)查找
                context = context.parent;
            }
            cache[name] = value;
        }

        // 如果查找到的內(nèi)容是 函數(shù),然后執(zhí)行到,賦值給value
        if (isFunction(value))
            value = value.call(this.view);

        // 返回找到到的內(nèi)容
        return value;
    };

Writer

提供render方法。 把模板轉(zhuǎn)換為tokens。然后處理只有的tokens 通過(guò)數(shù)據(jù)轉(zhuǎn)換為瀏覽器識(shí)別的字符串



    /**
     * 提供解析模板為tokens 然后把tokens 轉(zhuǎn)為 dom字符串
     * 根據(jù)tokens 轉(zhuǎn)換為 字符串,并且緩存它
     */
    function Writer() {
        this.cache = {};
    }
    /**
     * 清空緩存
     */
    Writer.prototype.clearCache = function () {
        this.cache = {};
    };
    /**
     * 解析和緩存給定的“模板”,并返回從解析生成的令牌數(shù)組。
     */
    Writer.prototype.parse = function (template, tags) {
        // 拿到緩存
        var cache = this.cache;
        // 如果緩存中有,直接返回對(duì)應(yīng)的tokens
        var tokens = cache[template];
        // 若果緩存中沒(méi)有。那么調(diào)用 parseTemplate返回獲取tokens并緩存起來(lái)
        if (tokens == null)
            tokens = cache[template] = parseTemplate(template, tags);
           
        return tokens;
    };
    /**
     * 渲染函數(shù) 
     * template 為模板
     * view 為數(shù)據(jù)
     * partials 為補(bǔ)充模板 可以為對(duì)象也可以為函數(shù)
     */
    Writer.prototype.render = function (template, view, partials) {
        // 把模板 轉(zhuǎn)化 tokens
        var tokens = this.parse(template);
        // 數(shù)據(jù)是不是 是不是 Context的示例,不是的話,用 Context實(shí)例化處理數(shù)據(jù)
        var context = (view instanceof Context) ? view : new Context(view);
        
        return this.renderTokens(tokens, context, partials, template);
    };
    /**
     * 遞歸函數(shù)  用于處理tokens  處理為dom字符串
     *     
     *  */
    Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
        var buffer = ''; // 初始為字符串
        
        var self = this;
        // 附屬渲染  獲取  頂層渲染的數(shù)據(jù)
        function subRender(template) {
            return self.render(template, context, partials);
        }
        var token, value;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            switch (token[0]) {
                // 如果是# 那這個(gè)token 就是循環(huán)的token 
                // 比如 ["#", "list", 194, 203, Array(5), 260]
                case '#':
                    // 數(shù)據(jù)中查找 list 對(duì)應(yīng)的 數(shù)據(jù)
                    value = context.lookup(token[1]);
                    console.log('value', value);
                    // 如果數(shù)據(jù)紅沒(méi)有則跳過(guò)
                    if (!value)
                        continue;
                    // 如果是 數(shù)組
                    // 比如  {list: [{name:'zs'},{name: 'ls'}]}
                    // value 為[{name:'zs'},{name: 'ls'}]
                    if (isArray(value)) {
                        for (var j = 0, valueLength = value.length; j < valueLength; ++j) {

                            // 遞歸 把 Array(5) 作為token, 每一項(xiàng) {name: 'xx'} 作為數(shù)據(jù)
                            buffer += this.renderTokens(token[4], context.push(value[j]), partials,
                                originalTemplate);
                        }
                        // 如果 找到的 value 為對(duì)象或字符串
                        // {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
                        // value 為 {name:'zs', age:14} 或 'abcd'
                    } else if (typeof value === 'object' || typeof value === 'string') {
                        // 也是遞歸 把 Array(5)作為token, 直接把當(dāng)前value作為數(shù)據(jù)
                        buffer += this.renderTokens(token[4], context.push(value), partials,
                            originalTemplate);
                        // 如果找到的函數(shù)
                        /**
                         *  arr: ()=> (template, fn)=>{
                         *     return fn(template)
                         *   }
                         * 
                         *  template  == originalTemplate.slice(token[3], token[5]) 為循環(huán)內(nèi)的位解析字符串
                         * fn == subRender 用根 數(shù)據(jù)渲染當(dāng)前模板
                         */
                    } else if (isFunction(value)) {
                        // 如果 原始模板不是字符串 則報(bào)錯(cuò)
                        if (typeof originalTemplate !== 'string')
                            throw new Error(
                                'Cannot use higher-order sections without the original template');
                        // 返回一個(gè)自定義函數(shù), 原函數(shù)匹配的字符串為第一個(gè)參數(shù) ,subRender為第二個(gè)參數(shù)(以根數(shù)據(jù)來(lái)渲染當(dāng)前模板)
                        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
                        
                        if (value != null)
                            buffer += value;
                    } else {
                        // 其他情況 直接調(diào)用 遞歸處理 并且不用 context.push反正創(chuàng)建子context
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    }
                    break;
                case '^':
                    // 查找到 數(shù)據(jù)中的對(duì)應(yīng)值 (lookup方法會(huì)找到最上層)
                    value = context.lookup(token[1]);
                    // 如果沒(méi)有找到值 或者 是一個(gè)空數(shù)組  調(diào)用 renderTokens 方法查找對(duì)應(yīng)數(shù)據(jù)
                    // 
                    if (!value || (isArray(value) && value.length === 0))
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    break;
                case '>':
                    // > 如果沒(méi)有 分部 partials 跳過(guò)
                    if (!partials)
                        continue;
                        // 判斷partials 是不是函數(shù),函數(shù)執(zhí)行 否則世界查找partials的 值
                    value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
                    if (value != null)
                         // 有值的話繼續(xù)遞歸
                        buffer += this.renderTokens(this.parse(value), context, partials, value);
                    break;
                case '&':
                    // 如果是& 說(shuō)明是{{{}}} 中的數(shù)據(jù), 去context 中查找到對(duì)應(yīng)的數(shù)據(jù) 后直接拼接
                    value = context.lookup(token[1]);
                    if (value != null)
                        buffer += value;
                    break;
                case 'name':
                    // name  去context 中查找到對(duì)應(yīng)的數(shù)據(jù)
                    value = context.lookup(token[1]);
                    // 如果不為空,轉(zhuǎn)義之后拼接
                    if (value != null)
                        buffer += mustache.escape(value);
                    break;
                case 'text':
                    // text 是文本 ,直接拼接
                    buffer += token[1];
                    break;
            }
        }
        return buffer;
    };

下面是整個(gè)文件的注解:

/*!
 * mustache.js - Logic-less {{mustache}} templates with JavaScript
 * http://github.com/janl/mustache.js
 */
/*global define: false*/
// umd 寫(xiě)法: 兼容AMD和commonJS規(guī)范的同時(shí),還兼容全局引用的方式
(function (global, factory) {
    if (typeof exports === "object" && exports) {
        factory(exports); // CommonJS
    } else if (typeof define === "function" && define.amd) {
        define(['exports'], factory); // AMD
    } else {
        factory(global.Mustache = {}); // <script>
    }
}(this, function (mustache) {


    mustache.name = "mustache.js";
    mustache.version = "1.0.0";
    mustache.tags = ["{{", "}}"]; //  模板的標(biāo)識(shí)


    var whiteRe = /\s*/; // 空格可能存在
    var spaceRe = /\s+/; // 最少有一個(gè)空格
    var equalsRe = /\s*=/; // 等于號(hào)前面可能存在空格
    var curlyRe = /\s*\}/; // 等于號(hào)前面最少存在一個(gè)空格
    var tagRe = /#|\^|\/|>|\{|&|=|!/; // 最少存在匹配里面一個(gè)符號(hào)


    

    // 導(dǎo)出三個(gè)類(lèi)主要用于測(cè)試,但也用于高級(jí)用途
    mustache.Scanner = Scanner;
    mustache.Context = Context;
    mustache.Writer = Writer;

    mustache.escape = escapeHtml; // escape 作用是把特殊符號(hào)轉(zhuǎn)移
    /*** escape start ****/
    var entityMap = {
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        '"': '&quot;',
        "'": '&#39;',
        "/": '&#x2F;'
    };
    function escapeHtml(string) {
        return String(string).replace(/[&<>"'\/]/g, function (s) {
            return entityMap[s];
        });
    }
    /*** escape end ****/


    /*** 判斷是否是數(shù)組 -statrt*****/ 
    var Object_toString = Object.prototype.toString;
    var isArray = Array.isArray || function (object) {
        return Object_toString.call(object) === '[object Array]';
    };
    /*** 判斷是否是數(shù)組 -end*****/ 


    /*** 判斷是否是函數(shù) -statrt*****/ 
    function isFunction(object) {
        return typeof object === 'function';
    }
     /*** 判斷是否是函數(shù) -end*****/ 


     // $&: 與 regexp 相匹配的子串
     // escapeRegExp是把匹配到的符號(hào)  前加一個(gè)反斜杠
     /**拓展: 
      * $1、$2、...、$99: 與 regexp 中的第 1 到第 99 個(gè)子表達(dá)式相匹配的文本
      * $&:與 regexp 相匹配的子串
      * $`:位于匹配子串左側(cè)的文本。
      * $':位于匹配子串右側(cè)的文本。
      * $$:直接量符號(hào)。
      * */ 
    function escapeRegExp(string) {
        return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
    }



    /**** 正則 isWhitespace 是空白(空格、換行、tab縮進(jìn)等所有的空白) start  *****/ 
    var RegExp_test = RegExp.prototype.test;
    function testRegExp(re, string) {
        return RegExp_test.call(re, string);
    } 

    var nonSpaceRe = /\S/;
    function isWhitespace(string) {
        return !testRegExp(nonSpaceRe, string);
    }
    /**** 正則 isWhitespace 是空白(空格、換行、tab縮進(jìn)等所有的空白)  end  *****/ 




    // 實(shí)例化  Writer函數(shù)
    var defaultWriter = new Writer();
    /**
     * 清除緩存
     */
    mustache.clearCache = function () {
        return defaultWriter.clearCache();
    };
    /**
     * 把模板處理為tokens
     */
    mustache.parse = function (template, tags) {
        return defaultWriter.parse(template, tags);
    };
    /**
     * 暴露的render 方法為實(shí)例化Writer之后的render方法
     */
    mustache.render = function (template, view, partials) {
        return defaultWriter.render(template, view, partials);
    };
    // This is here for backwards compatibility with 0.4.x.
    // 0.4.x 的方法 多一個(gè)send的. 如果send 是函數(shù),需要執(zhí)行回調(diào)函數(shù)
    mustache.to_html = function (template, view, partials, send) {
        var result = mustache.render(template, view, partials);
        if (isFunction(send)) {
            send(result);
        } else {
            return result;
        }
    };


    /**
     *  處理模板為tokens
     */
    function parseTemplate(template, tags) {
        // 如果沒(méi)有傳入模板,直接返回空數(shù)組
        if (!template)
            return [];
        var sections = []; // 棧(先進(jìn)后出,后進(jìn)先出)
        var tokens = []; // 獲取的tokens  (會(huì)切割得很細(xì),一個(gè)空格也會(huì)是一個(gè)token)
        var spaces = []; // 這個(gè)數(shù)據(jù)會(huì)保存 tokens 中 保存的是空格 的位置
        var hasTag = false; // 當(dāng)前行上有{{tag}}嗎?
        var nonSpace = false; // 當(dāng)前行中是否有非空格字符?
        // Strips all whitespace tokens array for the current line
        // if there was a {{#tag}} on it and otherwise only space.

        // 處理空格 此時(shí) 每一項(xiàng)token 都是一個(gè)字符串
        function stripSpace() {
            // 如果有 匹配符號(hào) 并且 有個(gè)字符
            if (hasTag && !nonSpace) {
                // 根據(jù) 保存的空格 位置, 刪除tokens中的空格token
                while (spaces.length)
                    delete tokens[spaces.pop()];
            } else {
                spaces = [];
            }
            // 把 匹配符號(hào) 標(biāo)志重置為fals。 nonSpace為false
            hasTag = false;
            nonSpace = false;
        }
        var openingTagRe, closingTagRe, closingCurlyRe;
        // 根據(jù)傳入的符號(hào) 設(shè)置標(biāo)簽
        function compileTags(tags) {
            // 如果 匹配符號(hào)是 字符串, 說(shuō)明要用自定義的解析符號(hào) 
            // 比如 <% %>  安裝空字符串切個(gè)我一個(gè)對(duì)象 ["<%", ">%"]
            if (typeof tags === 'string')
                tags = tags.split(spaceRe, 2);
            // 如果tags 不是一個(gè)數(shù)組,或者 是數(shù)組,但是不是兩位直接報(bào)錯(cuò)
            if (!isArray(tags) || tags.length !== 2)
                throw new Error('Invalid tags: ' + tags);
            // 下面以解析符號(hào)獲取正則表達(dá)式
            //  /\{\{\s*/   兩個(gè)左大括號(hào) 后可能有空格 
            openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
            //  /\s*\}\}/   兩個(gè)右大括號(hào) 前可能有空格 
            closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
            //  /\s*\}\}\}/ 三個(gè)右大括號(hào) 前可能有空格 
            closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
        }
        // 處理tags, 如果沒(méi)有,那就用默認(rèn) tags ["{{", "}}"]
        compileTags(tags || mustache.tags);
        // 得到 Scanner類(lèi)的示例
        var scanner = new Scanner(template);
        
        var start, type, value, chr, token, openSection;
        // 循環(huán)遍歷 scanner 
        while (!scanner.eos()) {
            // 獲取 位置 pos 會(huì)根據(jù)scan 和scanUtil 變化
            start = scanner.pos;
            // 找到 {{ 開(kāi)始符號(hào)前的文本
            value = scanner.scanUntil(openingTagRe);
            // 把獲取到的文本處理
            if (value) {
                // 把文本字符串 從第一個(gè)字符開(kāi)始遍歷 比如 "我是中國(guó)人   我愛(ài)中國(guó)"
                for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
                    // 獲取當(dāng)前字符串
                    chr = value.charAt(i);
                    // 如果當(dāng)前字符為空字符 當(dāng)前tokens的長(zhǎng)度 放到spaces數(shù)組中
                    // tokens每一項(xiàng)放的都是一個(gè) 字符,sapces會(huì)記錄哪幾項(xiàng)是空格
                    if (isWhitespace(chr)) {
                        spaces.push(tokens.length);
                    } else {
                        // 如果不是 空字符   設(shè)置 當(dāng)前行中沒(méi)有空格字符
                        nonSpace = true;
                    }
                    // 把當(dāng)前字符 以 數(shù)組放入tokens中 并記錄當(dāng)前位置和結(jié)束位置
                    tokens.push(['text', chr, start, start + 1]);
                    
                    start += 1;
                    // 如果有回車(chē)
                    if (chr === '\n')
                    // 去掉空格
                        stripSpace();
                }
            }
            // 如果沒(méi)有 {{ , 說(shuō)明已經(jīng)找完了 結(jié)束循環(huán)
            if (!scanner.scan(openingTagRe))
                break;
            // 如果沒(méi)有終止,說(shuō)明有 {{`
            hasTag = true;
            // 處理 循環(huán) 注釋等其他 類(lèi)型
            // 根據(jù) /#|\^|\/|>|\{|&|=|!/ 獲取 如果沒(méi)有匹配到,那就是默認(rèn)的數(shù)據(jù)類(lèi)型 name
            // 可能是 循環(huán)開(kāi)始#  循環(huán)結(jié)束\ 注釋?zhuān)〔挥棉D(zhuǎn)義{ 等
            type = scanner.scan(tagRe) || 'name';            
            
            // 跳過(guò)空字符串
            scanner.scan(whiteRe);

            // 如果type 為 = 符號(hào)
            if (type === '=') {
                // value 保存 新的 匹配符號(hào)  比如 {{=<% %>=}} 獲取到 value = "<% %>"
                value = scanner.scanUntil(equalsRe);
                scanner.scan(equalsRe);
                scanner.scanUntil(closingTagRe);
            } else if (type === '{') { // {
                // value 或 }}} 前的內(nèi)容
                value = scanner.scanUntil(closingCurlyRe);
                scanner.scan(curlyRe);
                scanner.scanUntil(closingTagRe);
                // 并且把類(lèi)型重置為 & 符號(hào)
                type = '&';
            } else {
                // 其他情況 獲取結(jié)束符號(hào)}} 前的文本 
                value = scanner.scanUntil(closingTagRe);
            }
            // 匹配結(jié)束符號(hào)
            if (!scanner.scan(closingTagRe))
                throw new Error('Unclosed tag at ' + scanner.pos);
            // 設(shè)置每一項(xiàng)token [類(lèi)型, 值, 開(kāi)始位置, scanner的位置]
            token = [type, value, start, scanner.pos];
            
            // 推到tokens中
            tokens.push(token);

            // 如果 # 或者 ^ 符號(hào) 推入 棧sections中
            if (type === '#' || type === '^') {
                sections.push(token);
            } else if (type === '/') {
                // 遇到結(jié)束符號(hào) / 把 棧里面取出第一個(gè)
                openSection = sections.pop();
                // 如果沒(méi)有內(nèi)容,報(bào)錯(cuò)
                if (!openSection)
                    throw new Error('Unopened section "' + value + '" at ' + start);
                // 如果內(nèi)容 [type, value, start, scanner.pos] 如果值不相等報(bào)錯(cuò)
                if (openSection[1] !== value)
                    throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
            } else if (type === 'name' || type === '{' || type === '&') {
                // 當(dāng)前行中 無(wú)空格字符
                nonSpace = true;
            } else if (type === '=') {
                // 為下一次設(shè)置標(biāo)簽
                compileTags(value);
            }
        }
        // 完成后確保沒(méi)有打開(kāi)的部分。
        openSection = sections.pop();
        if (openSection)
            throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);

        return nestTokens(squashTokens(tokens));
    }
    /**
     * 將給定“tokens”數(shù)組中連續(xù)文本標(biāo)記的值合并為單個(gè)標(biāo)記。
     * 把相鄰的token 切 type都是text類(lèi)型的文本合并為一個(gè)token
     */
    function squashTokens(tokens) {
        var squashedTokens = []; //定義壓縮數(shù)組
        var token, lastToken;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            // token 存在  [type, value, start, scanner.pos]
            if (token) {

                // 如果當(dāng)前token類(lèi)型為 text 。 并且上一個(gè)token(現(xiàn)在的lastToken) 類(lèi)型也是text
                // 那么就把兩個(gè)token合并
                // 這里的處理 把value 現(xiàn)在, scanner.pos 位置為現(xiàn)在token的位置
                if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
                    lastToken[1] += token[1];
                    lastToken[3] = token[3];
                } else {
                    // 第一項(xiàng) token 存起來(lái), 并且賦值給lastToken
                    // 或者現(xiàn)在的token和上一個(gè)token type不一樣
                    squashedTokens.push(token);
                    lastToken = token;
                }
            }
        }
        return squashedTokens;
    }
    /**
     * 處理 # 開(kāi)始 的循環(huán)接口。處理為嵌套的token
     * 把 type 為 # 的開(kāi)始token ,結(jié)束為 '/'結(jié)束的token。 這一堆token作為數(shù)組,作為#token的第五項(xiàng)
     * 
     * 這里要注意  收集器 的作用主要是 改變 一個(gè)指向。如要 # 就要把收集器指向 當(dāng)前token的 第五項(xiàng)
     * 當(dāng)遇到 / 說(shuō)明循環(huán)結(jié)束了。需要把收集器指向上一級(jí)的token的第五項(xiàng)
     * 循環(huán)一直,只到?jīng)]有循環(huán)為止,指向最初的token;
     */
    function nestTokens(tokens) {
        var nestedTokens = []; // 嵌套tokens
        var collector = nestedTokens; // 收集器 指向每一次需要收集的數(shù)組
        var sections = []; // 棧數(shù)組 用于 保存有多少次循環(huán), 并存在循環(huán)的主token
        var token, section;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            switch (token[0]) {
                // 如果type 為 # ^ 那就是嵌套開(kāi)始項(xiàng)
                // [type, value, start, scanner.pos, [嵌套token]]
                case '#':
                case '^':
                    // 收集器push 進(jìn)token
                    collector.push(token);
                    // 棧中 推入當(dāng)前token 這個(gè)是類(lèi)似一個(gè)父token
                    sections.push(token);
                    // 收集器 現(xiàn)在指向 當(dāng)前token 第五項(xiàng)并且為空數(shù)組
                    // 只有遇到的token都要放入這個(gè)空數(shù)組中
                    collector = token[4] = [];
                    break;
                    // 如果遇到/ 那么就是 循環(huán)結(jié)束了。 需要
                case '/':
                    // 結(jié)束了的話,把最后的一次棧中的token 取出來(lái)
                    section = sections.pop();
                    // 把 當(dāng)前token 的scanner.pos 給 父token的第六項(xiàng) 
                    section[5] = token[2];
                    // 判斷棧中還有沒(méi)有數(shù)據(jù)。沒(méi)有的話,就是初始nestedTokens;否則,
                    // 收集器指針 需要指向上一級(jí)父token的第五位
                    collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
                    break;
                default:
                    // 循環(huán)中 把每一個(gè)token 放入收集器中
                    collector.push(token);
            }
        }
        // 返回 處理過(guò)的循環(huán)tokens
        return nestedTokens;
    }









    /**
     * 模板解析器用于在模板字符串中查找令牌的簡(jiǎn)單字符串掃描儀
     * 獲取字符串,并設(shè)置尾部,尾巴會(huì)向后減少。pos是當(dāng)前位置
     */
    function Scanner(string) {
        this.string = string;
        this.tail = string;
        this.pos = 0;
    }
    /**
     * 判斷是否到尾部
     */
    Scanner.prototype.eos = function () {
        return this.tail === "";
    };
    /**
     * 為了跳過(guò)匹配符號(hào)
     * 返回匹配的匹配的符號(hào),如果沒(méi)有返回空字符串
     * 比如 re為 {{   找到的話就返回 {{  并且尾巴向后移動(dòng)兩位,pos位置也加2
     */
    Scanner.prototype.scan = function (re) {
        var match = this.tail.match(re);
        // 沒(méi)有匹配直接返回空字符串
        if (!match || match.index !== 0)
            return '';
        var string = match[0];
        // 尾巴 向后移動(dòng) 匹配符號(hào)的長(zhǎng)度
        this.tail = this.tail.substring(string.length);
        // 位置加 上匹配符號(hào)的長(zhǎng)度
        this.pos += string.length;
        return string;
    };
    /**
     * 為了找到 匹配符號(hào)前的文本
     * 如果有匹配的符號(hào),返回匹配符號(hào)之前的文本,否則就是沒(méi)有匹配到,返回剩下的尾巴
     * 比如 這里是{{name}}的家  re為 {{  會(huì)返回 這里是
     */
    Scanner.prototype.scanUntil = function (re) {
        // 得到匹配符號(hào)在尾巴中的位置(尾巴會(huì)變化,所有位置也會(huì)在變化)
        var index = this.tail.search(re),
            match;
        switch (index) {
            // -1說(shuō)明沒(méi)有匹配到特殊符號(hào),說(shuō)明已經(jīng)掃描完了。直接返回剩下的文本。比把尾巴置空
            case -1:
                match = this.tail;
                this.tail = "";
                break;
            // 特殊符號(hào)剛好在開(kāi)始地方,那匹配符號(hào)前 就沒(méi)有文本。匹配到的就是空字符串
            case 0:
                match = "";
                break;
            // 其他情況 直接獲取匹配符號(hào)前的文本,并把尾巴向后移動(dòng)到匹配的地方(這個(gè)時(shí)候后面會(huì)用scan函數(shù)跳過(guò)匹配符號(hào))
            default:
                match = this.tail.substring(0, index);
                this.tail = this.tail.substring(index);
        }
        // 位置 需要加上匹配的長(zhǎng)度 ,并返回匹配的文本 
        this.pos += match.length;
        return match;
    };










    /**
     * 
     * view 類(lèi) 數(shù)據(jù) 比如 {{name: 'zs', age: 14}}
     * 把數(shù)據(jù)作為參數(shù) 實(shí)例化Context 。便于 創(chuàng)建新的數(shù)據(jù) 并且根據(jù)樹(shù)結(jié)構(gòu)查找數(shù)據(jù)
     */
    function Context(view, parentContext) {
        // view 如果不存在,默認(rèn)設(shè)置為空對(duì)象
        this.view = view == null ? {} : view;
        // 緩存  對(duì)象  .
        this.cache = {
            '.': this.view
        };
        // 設(shè)置父元素為 傳入的第二個(gè)參數(shù)
        this.parent = parentContext;
    }
    /**
     * 使用給定視圖創(chuàng)建一個(gè)新上下文,并將此上下文作為父級(jí)。
     */
    Context.prototype.push = function (view) {
        return new Context(view, this);
    };
    /**
     * 返回此上下文中給定名稱的值,如果此上下文視圖中缺少該值,則遍歷上下文層次結(jié)構(gòu)。
     */
    Context.prototype.lookup = function (name) {
        // 首先拿到緩存
        var cache = this.cache;
        // 定義查找的內(nèi)容
        var value;
        // 如果要查找的 內(nèi)容在緩存中  賦值為value
        // 如果是循環(huán) 的情況,獲取 .  這個(gè)符號(hào),會(huì)直接返回 上面 有緩存對(duì)象
        if (name in cache) {
            value = cache[name];
        } else {
            // context 初始定義為 當(dāng)前對(duì)象,下面可能會(huì)改變
            var context = this,
                names, index;
            // 循環(huán)遍歷 context ,知道context不存在為止
            while (context) {
                // 如果要查找的是 a.b.c 類(lèi)似 的數(shù)據(jù)
                if (name.indexOf('.') > 0) {
                    // 首先把 數(shù)據(jù)賦值為value  比如 { a: { b: c: 'haha' } }
                    value = context.view;
                    // names 把 a.b.c 拆分為 [a.b.c]
                    names = name.split('.');
                    // index 初始為0
                    index = 0;
                    //  循環(huán) names 數(shù)組 ,并分別從value中查找到值,并重新賦值為value
                    //  比如上面最后會(huì)得到 value 為 haha
                    while (value != null && index < names.length)
                        value = value[names[index++]];

                } 
                // 如果 查找的關(guān)鍵字沒(méi)有點(diǎn)  并且 數(shù)據(jù)源是對(duì)象。那么直接返回匹配的值
                else if (typeof context.view == 'object') {
                    value = context.view[name];
                }
                // 如果找到匹配值,那么終止循環(huán)
                if (value != null)
                    break;
                // 如果沒(méi)有找到匹配的值,那么把context 上下文重新定義為 父 上下文,并重新循環(huán)查找
                context = context.parent;
            }
            cache[name] = value;
        }

        // 如果查找到的內(nèi)容是 函數(shù),然后執(zhí)行到,賦值給value
        if (isFunction(value))
            value = value.call(this.view);

        // 返回找到到的內(nèi)容
        return value;
    };












    /**
     * 提供解析模板為tokens 然后把tokens 轉(zhuǎn)為 dom字符串
     * 根據(jù)tokens 轉(zhuǎn)換為 字符串,并且緩存它
     */
    function Writer() {
        this.cache = {};
    }
    /**
     * 清空緩存
     */
    Writer.prototype.clearCache = function () {
        this.cache = {};
    };
    /**
     * 解析和緩存給定的“模板”,并返回從解析生成的令牌數(shù)組。
     */
    Writer.prototype.parse = function (template, tags) {
        // 拿到緩存
        var cache = this.cache;
        // 如果緩存中有,直接返回對(duì)應(yīng)的tokens
        var tokens = cache[template];
        // 若果緩存中沒(méi)有。那么調(diào)用 parseTemplate返回獲取tokens并緩存起來(lái)
        if (tokens == null)
            tokens = cache[template] = parseTemplate(template, tags);
           
        return tokens;
    };
    /**
     * 渲染函數(shù) 
     * template 為模板
     * view 為數(shù)據(jù)
     * partials 為補(bǔ)充模板 可以為對(duì)象也可以為函數(shù)
     */
    Writer.prototype.render = function (template, view, partials) {
        // 把模板 轉(zhuǎn)化 tokens
        var tokens = this.parse(template);
        // 數(shù)據(jù) 是不是 Context的實(shí)例,不是的話,用 Context實(shí)例化處理數(shù)據(jù)
        var context = (view instanceof Context) ? view : new Context(view);
        
        return this.renderTokens(tokens, context, partials, template);
    };
    /**
     * 遞歸函數(shù)  用于處理tokens  處理為dom字符串
     *     
     *  */
    Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
        var buffer = ''; // 初始為字符串
        
        var self = this;
        // 附屬渲染  獲取  頂層渲染的數(shù)據(jù)
        function subRender(template) {
            return self.render(template, context, partials);
        }
        var token, value;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            switch (token[0]) {
                // 如果是# 那這個(gè)token 就是循環(huán)的token 
                // 比如 ["#", "list", 194, 203, Array(5), 260]
                case '#':
                    // 數(shù)據(jù)中查找 list 對(duì)應(yīng)的 數(shù)據(jù)
                    value = context.lookup(token[1]);
                    // 如果數(shù)據(jù)沒(méi)有則跳過(guò)
                    if (!value)
                        continue;
                    // 如果是 數(shù)組
                    // 比如  {list: [{name:'zs'},{name: 'ls'}]}
                    // value 為[{name:'zs'},{name: 'ls'}]
                    if (isArray(value)) {
                        for (var j = 0, valueLength = value.length; j < valueLength; ++j) {

                            // 遞歸 把 Array(5) 作為token, 每一項(xiàng) {name: 'xx'} 作為數(shù)據(jù)
                            buffer += this.renderTokens(token[4], context.push(value[j]), partials,
                                originalTemplate);
                        }
                        // 如果 找到的 value 為對(duì)象或字符串
                        // {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
                        // value 為 {name:'zs', age:14} 或 'abcd'
                    } else if (typeof value === 'object' || typeof value === 'string') {
                        // 也是遞歸 把 Array(5)作為token, 直接把當(dāng)前value作為數(shù)據(jù)
                        buffer += this.renderTokens(token[4], context.push(value), partials,
                            originalTemplate);
                        // 如果找到的函數(shù)
                        /**
                         *  arr: ()=> (template, fn)=>{
                         *     return fn(template)
                         *   }
                         * 
                         *  template  == originalTemplate.slice(token[3], token[5]) 為循環(huán)內(nèi)的位解析字符串
                         * fn == subRender 用根 數(shù)據(jù)渲染當(dāng)前模板
                         */
                    } else if (isFunction(value)) {
                        // 如果 原始模板不是字符串 則報(bào)錯(cuò)
                        if (typeof originalTemplate !== 'string')
                            throw new Error(
                                'Cannot use higher-order sections without the original template');
                        // 返回一個(gè)自定義函數(shù), 原函數(shù)匹配的字符串為第一個(gè)參數(shù) ,subRender為第二個(gè)參數(shù)(以根數(shù)據(jù)來(lái)渲染當(dāng)前模板)
                        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
                        
                        if (value != null)
                            buffer += value;
                    } else {
                        // 其他情況 直接調(diào)用 遞歸處理 并且不用 context.push反正創(chuàng)建子context
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    }
                    break;
                case '^':
                    // 查找到 數(shù)據(jù)中的對(duì)應(yīng)值 (lookup方法會(huì)找到最上層)
                    value = context.lookup(token[1]);
                    // 如果沒(méi)有找到值 或者 是一個(gè)空數(shù)組  調(diào)用 renderTokens 方法查找對(duì)應(yīng)數(shù)據(jù)
                    
                    if (!value || (isArray(value) && value.length === 0))
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    break;
                case '>':
                    // > 如果沒(méi)有 分部 partials 跳過(guò)
                    if (!partials)
                        continue;
                        // 判斷partials 是不是函數(shù),函數(shù)執(zhí)行 否則世界查找partials的 值
                    value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
                    if (value != null)
                         // 有值的話繼續(xù)遞歸
                        buffer += this.renderTokens(this.parse(value), context, partials, value);
                    break;
                case '&':
                    // 如果是& 說(shuō)明是{{{}}} 中的數(shù)據(jù), 去context 中查找到對(duì)應(yīng)的數(shù)據(jù) 后直接拼接
                    value = context.lookup(token[1]);
                    if (value != null)
                        buffer += value;
                    break;
                case 'name':
                    // name  去context 中查找到對(duì)應(yīng)的數(shù)據(jù)
                    value = context.lookup(token[1]);
                    // 如果不為空,轉(zhuǎn)義之后拼接
                    if (value != null)
                        buffer += mustache.escape(value);
                    break;
                case 'text':
                    // text 是文本 ,直接拼接
                    buffer += token[1];
                    break;
            }
        }
        return buffer;
    };

}));

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容