mustache-1.0源码注解

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

大概的方法

{{name}} 会把数据中 name 转换为具体值
{{#list}} {{/list}} 会把list中的数据选项展示
{{^list}}{{/list}} 数据中 list不存在或者未空数组才展示
{{!注释}} 注释
{{{}}} 里面的内容不会转义
{{>name}} 会从附属数据中获取数据
<div>{{=<% %>=}}</div> 修改 <% %> 为新切割符号

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>{{<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: [], //也可以不要这一项
        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模板 通过 对应的分隔符号 比如 {{ }} 转为为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 是这个 类型 text 为文本, name需要取数据中的键 ,# 是循环
  • value 数据中的键或者字符串
  • start 这个是当前数组所在字符串的位置
  • scanner.pos 扫描器的位置或者未结束位置

然后把tokens处理为 dom识别的字符串;

源码里面有三个类:

  • Scanner;
  • Context;
  • Writer;

Scanner 扫描器

我的理解 Scanner 是 当 匹配符号比如 {{ 和 }} 匹配一段文本是分别 获取 匹配符号之外的文本。(其实跟以前版本正则类似)
比例: 我是{{name}},我爱我的{{contry}}。也爱你
通过Scanner类可以获取到 五段内容:

  1. 我是
  2. name
  3. ,我爱我的
  4. contry
  5. 。也爱你

源码


    /**
     * 模板解析器用于在模板字符串中查找令牌的简单字符串扫描仪
     * 获取字符串,并设置尾部,尾巴会向后减少。pos是当前位置
     */
    function Scanner(string) {
        this.string = string;
        this.tail = string;
        this.pos = 0;
    }
    /**
     * 判断是否到尾部
     */
    Scanner.prototype.eos = function () {
        return this.tail === "";
    };
    /**
     * 为了跳过匹配符号
     * 返回匹配的匹配的符号,如果没有返回空字符串
     * 比如 re为 {{   找到的话就返回 {{  并且尾巴向后移动两位,pos位置也加2
     */
    Scanner.prototype.scan = function (re) {
        var match = this.tail.match(re);
        // 没有匹配直接返回空字符串
        if (!match || match.index !== 0)
            return '';
        var string = match[0];
        // 尾巴 向后移动 匹配符号的长度
        this.tail = this.tail.substring(string.length);
        // 位置加 上匹配符号的长度
        this.pos += string.length;
        return string;
    };
    /**
     * 为了找到 匹配符号前的文本
     * 如果有匹配的符号,返回匹配符号之前的文本,否则就是没有匹配到,返回剩下的尾巴
     * 比如 这里是{{name}}的家  re为 {{  会返回 这里是
     */
    Scanner.prototype.scanUntil = function (re) {
        // 得到匹配符号在尾巴中的位置(尾巴会变化,所有位置也会在变化)
        var index = this.tail.search(re),
            match;
        switch (index) {
            // -1说明没有匹配到特殊符号,说明已经扫描完了。直接返回剩下的文本。比把尾巴置空
            case -1:
                match = this.tail;
                this.tail = "";
                break;
            // 特殊符号刚好在开始地方,那匹配符号前 就没有文本。匹配到的就是空字符串
            case 0:
                match = "";
                break;
            // 其他情况 直接获取匹配符号前的文本,并把尾巴向后移动到匹配的地方(这个时候后面会用scan函数跳过匹配符号)
            default:
                match = this.tail.substring(0, index);
                this.tail = this.tail.substring(index);
        }
        // 位置 需要加上匹配的长度 ,并返回匹配的文本 
        this.pos += match.length;
        return match;
    };

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>

由于这个类向外暴露了。我们可以直接测试


image.png

这里我只看一下效果,后面代码里面会循环调用;

Context 上下文

作用是用来处理data数据,便于查找对应的数据

/**
     * 
     * view 类 数据 比如 {{name: 'zs', age: 14}}
     * 把数据作为参数 实例化Context 。便于 创建新的数据 并且根据树结构查找数据
     */
    function Context(view, parentContext) {
        // view 如果不存在,默认设置为空对象
        this.view = view == null ? {} : view;
        // 缓存  对象  .
        this.cache = {
            '.': this.view
        };
        // 设置父元素为 传入的第二个参数
        this.parent = parentContext;
    }
    /**
     * 使用给定视图创建一个新上下文,并将此上下文作为父级。
     */
    Context.prototype.push = function (view) {
        return new Context(view, this);
    };
    /**
     * 返回此上下文中给定名称的值,如果此上下文视图中缺少该值,则遍历上下文层次结构。
     */
    Context.prototype.lookup = function (name) {
        // 首先拿到缓存
        var cache = this.cache;
        // 定义查找的内容
        var value;
        // 如果要查找的 内容在缓存中  赋值为value
        // 如果是循环 的情况,获取 .  这个符号,会直接返回 上面 有缓存对象
        if (name in cache) {
            value = cache[name];
        } else {
            // context 初始定义为 当前对象,下面可能会改变
            var context = this,
                names, index;
            // 循环遍历 context ,知道context不存在为止
            while (context) {
                // 如果要查找的是 a.b.c 类似 的数据
                if (name.indexOf('.') > 0) {
                    // 首先把 数据赋值为value  比如 { a: { b: c: 'haha' } }
                    value = context.view;
                    // names 把 a.b.c 拆分为 [a.b.c]
                    names = name.split('.');
                    // index 初始为0
                    index = 0;
                    //  循环 names 数组 ,并分别从value中查找到值,并重新赋值为value
                    //  比如上面最后会得到 value 为 haha
                    while (value != null && index < names.length)
                        value = value[names[index++]];

                } 
                // 如果 查找的关键字没有点  并且 数据源是对象。那么直接返回匹配的值
                else if (typeof context.view == 'object') {
                    value = context.view[name];
                }
                // 如果找到匹配值,那么终止循环
                if (value != null)
                    break;
                // 如果没有找到匹配的值,那么把context 上下文重新定义为 父 上下文,并重新循环查找
                context = context.parent;
            }
            cache[name] = value;
        }

        // 如果查找到的内容是 函数,然后执行到,赋值给value
        if (isFunction(value))
            value = value.call(this.view);

        // 返回找到到的内容
        return value;
    };

Writer

提供render方法。 把模板转换为tokens。然后处理只有的tokens 通过数据转换为浏览器识别的字符串



    /**
     * 提供解析模板为tokens 然后把tokens 转为 dom字符串
     * 根据tokens 转换为 字符串,并且缓存它
     */
    function Writer() {
        this.cache = {};
    }
    /**
     * 清空缓存
     */
    Writer.prototype.clearCache = function () {
        this.cache = {};
    };
    /**
     * 解析和缓存给定的“模板”,并返回从解析生成的令牌数组。
     */
    Writer.prototype.parse = function (template, tags) {
        // 拿到缓存
        var cache = this.cache;
        // 如果缓存中有,直接返回对应的tokens
        var tokens = cache[template];
        // 若果缓存中没有。那么调用 parseTemplate返回获取tokens并缓存起来
        if (tokens == null)
            tokens = cache[template] = parseTemplate(template, tags);
           
        return tokens;
    };
    /**
     * 渲染函数 
     * template 为模板
     * view 为数据
     * partials 为补充模板 可以为对象也可以为函数
     */
    Writer.prototype.render = function (template, view, partials) {
        // 把模板 转化 tokens
        var tokens = this.parse(template);
        // 数据是不是 是不是 Context的示例,不是的话,用 Context实例化处理数据
        var context = (view instanceof Context) ? view : new Context(view);
        
        return this.renderTokens(tokens, context, partials, template);
    };
    /**
     * 递归函数  用于处理tokens  处理为dom字符串
     *     
     *  */
    Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
        var buffer = ''; // 初始为字符串
        
        var self = this;
        // 附属渲染  获取  顶层渲染的数据
        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]) {
                // 如果是# 那这个token 就是循环的token 
                // 比如 ["#", "list", 194, 203, Array(5), 260]
                case '#':
                    // 数据中查找 list 对应的 数据
                    value = context.lookup(token[1]);
                    console.log('value', value);
                    // 如果数据红没有则跳过
                    if (!value)
                        continue;
                    // 如果是 数组
                    // 比如  {list: [{name:'zs'},{name: 'ls'}]}
                    // value 为[{name:'zs'},{name: 'ls'}]
                    if (isArray(value)) {
                        for (var j = 0, valueLength = value.length; j < valueLength; ++j) {

                            // 递归 把 Array(5) 作为token, 每一项 {name: 'xx'} 作为数据
                            buffer += this.renderTokens(token[4], context.push(value[j]), partials,
                                originalTemplate);
                        }
                        // 如果 找到的 value 为对象或字符串
                        // {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
                        // value 为 {name:'zs', age:14} 或 'abcd'
                    } else if (typeof value === 'object' || typeof value === 'string') {
                        // 也是递归 把 Array(5)作为token, 直接把当前value作为数据
                        buffer += this.renderTokens(token[4], context.push(value), partials,
                            originalTemplate);
                        // 如果找到的函数
                        /**
                         *  arr: ()=> (template, fn)=>{
                         *     return fn(template)
                         *   }
                         * 
                         *  template  == originalTemplate.slice(token[3], token[5]) 为循环内的位解析字符串
                         * fn == subRender 用根 数据渲染当前模板
                         */
                    } else if (isFunction(value)) {
                        // 如果 原始模板不是字符串 则报错
                        if (typeof originalTemplate !== 'string')
                            throw new Error(
                                'Cannot use higher-order sections without the original template');
                        // 返回一个自定义函数, 原函数匹配的字符串为第一个参数 ,subRender为第二个参数(以根数据来渲染当前模板)
                        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
                        
                        if (value != null)
                            buffer += value;
                    } else {
                        // 其他情况 直接调用 递归处理 并且不用 context.push反正创建子context
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    }
                    break;
                case '^':
                    // 查找到 数据中的对应值 (lookup方法会找到最上层)
                    value = context.lookup(token[1]);
                    // 如果没有找到值 或者 是一个空数组  调用 renderTokens 方法查找对应数据
                    // 
                    if (!value || (isArray(value) && value.length === 0))
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    break;
                case '>':
                    // > 如果没有 分部 partials 跳过
                    if (!partials)
                        continue;
                        // 判断partials 是不是函数,函数执行 否则世界查找partials的 值
                    value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
                    if (value != null)
                         // 有值的话继续递归
                        buffer += this.renderTokens(this.parse(value), context, partials, value);
                    break;
                case '&':
                    // 如果是& 说明是{{{}}} 中的数据, 去context 中查找到对应的数据 后直接拼接
                    value = context.lookup(token[1]);
                    if (value != null)
                        buffer += value;
                    break;
                case 'name':
                    // name  去context 中查找到对应的数据
                    value = context.lookup(token[1]);
                    // 如果不为空,转义之后拼接
                    if (value != null)
                        buffer += mustache.escape(value);
                    break;
                case 'text':
                    // text 是文本 ,直接拼接
                    buffer += token[1];
                    break;
            }
        }
        return buffer;
    };

下面是整个文件的注解:

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


    mustache.name = "mustache.js";
    mustache.version = "1.0.0";
    mustache.tags = ["{{", "}}"]; //  模板的标识


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


    

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

    mustache.escape = escapeHtml; // escape 作用是把特殊符号转移
    /*** 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 ****/


    /*** 判断是否是数组 -statrt*****/ 
    var Object_toString = Object.prototype.toString;
    var isArray = Array.isArray || function (object) {
        return Object_toString.call(object) === '[object Array]';
    };
    /*** 判断是否是数组 -end*****/ 


    /*** 判断是否是函数 -statrt*****/ 
    function isFunction(object) {
        return typeof object === 'function';
    }
     /*** 判断是否是函数 -end*****/ 


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



    /**** 正则 isWhitespace 是空白(空格、换行、tab缩进等所有的空白) start  *****/ 
    var RegExp_test = RegExp.prototype.test;
    function testRegExp(re, string) {
        return RegExp_test.call(re, string);
    } 

    var nonSpaceRe = /\S/;
    function isWhitespace(string) {
        return !testRegExp(nonSpaceRe, string);
    }
    /**** 正则 isWhitespace 是空白(空格、换行、tab缩进等所有的空白)  end  *****/ 




    // 实例化  Writer函数
    var defaultWriter = new Writer();
    /**
     * 清除缓存
     */
    mustache.clearCache = function () {
        return defaultWriter.clearCache();
    };
    /**
     * 把模板处理为tokens
     */
    mustache.parse = function (template, tags) {
        return defaultWriter.parse(template, tags);
    };
    /**
     * 暴露的render 方法为实例化Writer之后的render方法
     */
    mustache.render = function (template, view, partials) {
        return defaultWriter.render(template, view, partials);
    };
    // This is here for backwards compatibility with 0.4.x.
    // 0.4.x 的方法 多一个send的. 如果send 是函数,需要执行回调函数
    mustache.to_html = function (template, view, partials, send) {
        var result = mustache.render(template, view, partials);
        if (isFunction(send)) {
            send(result);
        } else {
            return result;
        }
    };


    /**
     *  处理模板为tokens
     */
    function parseTemplate(template, tags) {
        // 如果没有传入模板,直接返回空数组
        if (!template)
            return [];
        var sections = []; // 栈(先进后出,后进先出)
        var tokens = []; // 获取的tokens  (会切割得很细,一个空格也会是一个token)
        var spaces = []; // 这个数据会保存 tokens 中 保存的是空格 的位置
        var hasTag = false; // 当前行上有{{tag}}吗?
        var nonSpace = false; // 当前行中是否有非空格字符?
        // Strips all whitespace tokens array for the current line
        // if there was a {{#tag}} on it and otherwise only space.

        // 处理空格 此时 每一项token 都是一个字符串
        function stripSpace() {
            // 如果有 匹配符号 并且 有个字符
            if (hasTag && !nonSpace) {
                // 根据 保存的空格 位置, 删除tokens中的空格token
                while (spaces.length)
                    delete tokens[spaces.pop()];
            } else {
                spaces = [];
            }
            // 把 匹配符号 标志重置为fals。 nonSpace为false
            hasTag = false;
            nonSpace = false;
        }
        var openingTagRe, closingTagRe, closingCurlyRe;
        // 根据传入的符号 设置标签
        function compileTags(tags) {
            // 如果 匹配符号是 字符串, 说明要用自定义的解析符号 
            // 比如 <% %>  安装空字符串切个我一个对象 ["<%", ">%"]
            if (typeof tags === 'string')
                tags = tags.split(spaceRe, 2);
            // 如果tags 不是一个数组,或者 是数组,但是不是两位直接报错
            if (!isArray(tags) || tags.length !== 2)
                throw new Error('Invalid tags: ' + tags);
            // 下面以解析符号获取正则表达式
            //  /\{\{\s*/   两个左大括号 后可能有空格 
            openingTagRe = new RegExp(escapeRegExp(tags[0]) + '\\s*');
            //  /\s*\}\}/   两个右大括号 前可能有空格 
            closingTagRe = new RegExp('\\s*' + escapeRegExp(tags[1]));
            //  /\s*\}\}\}/ 三个右大括号 前可能有空格 
            closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tags[1]));
        }
        // 处理tags, 如果没有,那就用默认 tags ["{{", "}}"]
        compileTags(tags || mustache.tags);
        // 得到 Scanner类的示例
        var scanner = new Scanner(template);
        
        var start, type, value, chr, token, openSection;
        // 循环遍历 scanner 
        while (!scanner.eos()) {
            // 获取 位置 pos 会根据scan 和scanUtil 变化
            start = scanner.pos;
            // 找到 {{ 开始符号前的文本
            value = scanner.scanUntil(openingTagRe);
            // 把获取到的文本处理
            if (value) {
                // 把文本字符串 从第一个字符开始遍历 比如 "我是中国人   我爱中国"
                for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
                    // 获取当前字符串
                    chr = value.charAt(i);
                    // 如果当前字符为空字符 当前tokens的长度 放到spaces数组中
                    // tokens每一项放的都是一个 字符,sapces会记录哪几项是空格
                    if (isWhitespace(chr)) {
                        spaces.push(tokens.length);
                    } else {
                        // 如果不是 空字符   设置 当前行中没有空格字符
                        nonSpace = true;
                    }
                    // 把当前字符 以 数组放入tokens中 并记录当前位置和结束位置
                    tokens.push(['text', chr, start, start + 1]);
                    
                    start += 1;
                    // 如果有回车
                    if (chr === '\n')
                    // 去掉空格
                        stripSpace();
                }
            }
            // 如果没有 {{ , 说明已经找完了 结束循环
            if (!scanner.scan(openingTagRe))
                break;
            // 如果没有终止,说明有 {{`
            hasTag = true;
            // 处理 循环 注释等其他 类型
            // 根据 /#|\^|\/|>|\{|&|=|!/ 获取 如果没有匹配到,那就是默认的数据类型 name
            // 可能是 循环开始#  循环结束\ 注释!不用转义{ 等
            type = scanner.scan(tagRe) || 'name';            
            
            // 跳过空字符串
            scanner.scan(whiteRe);

            // 如果type 为 = 符号
            if (type === '=') {
                // value 保存 新的 匹配符号  比如 {{=<% %>=}} 获取到 value = "<% %>"
                value = scanner.scanUntil(equalsRe);
                scanner.scan(equalsRe);
                scanner.scanUntil(closingTagRe);
            } else if (type === '{') { // {
                // value 或 }}} 前的内容
                value = scanner.scanUntil(closingCurlyRe);
                scanner.scan(curlyRe);
                scanner.scanUntil(closingTagRe);
                // 并且把类型重置为 & 符号
                type = '&';
            } else {
                // 其他情况 获取结束符号}} 前的文本 
                value = scanner.scanUntil(closingTagRe);
            }
            // 匹配结束符号
            if (!scanner.scan(closingTagRe))
                throw new Error('Unclosed tag at ' + scanner.pos);
            // 设置每一项token [类型, 值, 开始位置, scanner的位置]
            token = [type, value, start, scanner.pos];
            
            // 推到tokens中
            tokens.push(token);

            // 如果 # 或者 ^ 符号 推入 栈sections中
            if (type === '#' || type === '^') {
                sections.push(token);
            } else if (type === '/') {
                // 遇到结束符号 / 把 栈里面取出第一个
                openSection = sections.pop();
                // 如果没有内容,报错
                if (!openSection)
                    throw new Error('Unopened section "' + value + '" at ' + start);
                // 如果内容 [type, value, start, scanner.pos] 如果值不相等报错
                if (openSection[1] !== value)
                    throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
            } else if (type === 'name' || type === '{' || type === '&') {
                // 当前行中 无空格字符
                nonSpace = true;
            } else if (type === '=') {
                // 为下一次设置标签
                compileTags(value);
            }
        }
        // 完成后确保没有打开的部分。
        openSection = sections.pop();
        if (openSection)
            throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);

        return nestTokens(squashTokens(tokens));
    }
    /**
     * 将给定“tokens”数组中连续文本标记的值合并为单个标记。
     * 把相邻的token 切 type都是text类型的文本合并为一个token
     */
    function squashTokens(tokens) {
        var squashedTokens = []; //定义压缩数组
        var token, lastToken;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            // token 存在  [type, value, start, scanner.pos]
            if (token) {

                // 如果当前token类型为 text 。 并且上一个token(现在的lastToken) 类型也是text
                // 那么就把两个token合并
                // 这里的处理 把value 现在, scanner.pos 位置为现在token的位置
                if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
                    lastToken[1] += token[1];
                    lastToken[3] = token[3];
                } else {
                    // 第一项 token 存起来, 并且赋值给lastToken
                    // 或者现在的token和上一个token type不一样
                    squashedTokens.push(token);
                    lastToken = token;
                }
            }
        }
        return squashedTokens;
    }
    /**
     * 处理 # 开始 的循环接口。处理为嵌套的token
     * 把 type 为 # 的开始token ,结束为 '/'结束的token。 这一堆token作为数组,作为#token的第五项
     * 
     * 这里要注意  收集器 的作用主要是 改变 一个指向。如要 # 就要把收集器指向 当前token的 第五项
     * 当遇到 / 说明循环结束了。需要把收集器指向上一级的token的第五项
     * 循环一直,只到没有循环为止,指向最初的token;
     */
    function nestTokens(tokens) {
        var nestedTokens = []; // 嵌套tokens
        var collector = nestedTokens; // 收集器 指向每一次需要收集的数组
        var sections = []; // 栈数组 用于 保存有多少次循环, 并存在循环的主token
        var token, section;
        for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
            token = tokens[i];
            switch (token[0]) {
                // 如果type 为 # ^ 那就是嵌套开始项
                // [type, value, start, scanner.pos, [嵌套token]]
                case '#':
                case '^':
                    // 收集器push 进token
                    collector.push(token);
                    // 栈中 推入当前token 这个是类似一个父token
                    sections.push(token);
                    // 收集器 现在指向 当前token 第五项并且为空数组
                    // 只有遇到的token都要放入这个空数组中
                    collector = token[4] = [];
                    break;
                    // 如果遇到/ 那么就是 循环结束了。 需要
                case '/':
                    // 结束了的话,把最后的一次栈中的token 取出来
                    section = sections.pop();
                    // 把 当前token 的scanner.pos 给 父token的第六项 
                    section[5] = token[2];
                    // 判断栈中还有没有数据。没有的话,就是初始nestedTokens;否则,
                    // 收集器指针 需要指向上一级父token的第五位
                    collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
                    break;
                default:
                    // 循环中 把每一个token 放入收集器中
                    collector.push(token);
            }
        }
        // 返回 处理过的循环tokens
        return nestedTokens;
    }









    /**
     * 模板解析器用于在模板字符串中查找令牌的简单字符串扫描仪
     * 获取字符串,并设置尾部,尾巴会向后减少。pos是当前位置
     */
    function Scanner(string) {
        this.string = string;
        this.tail = string;
        this.pos = 0;
    }
    /**
     * 判断是否到尾部
     */
    Scanner.prototype.eos = function () {
        return this.tail === "";
    };
    /**
     * 为了跳过匹配符号
     * 返回匹配的匹配的符号,如果没有返回空字符串
     * 比如 re为 {{   找到的话就返回 {{  并且尾巴向后移动两位,pos位置也加2
     */
    Scanner.prototype.scan = function (re) {
        var match = this.tail.match(re);
        // 没有匹配直接返回空字符串
        if (!match || match.index !== 0)
            return '';
        var string = match[0];
        // 尾巴 向后移动 匹配符号的长度
        this.tail = this.tail.substring(string.length);
        // 位置加 上匹配符号的长度
        this.pos += string.length;
        return string;
    };
    /**
     * 为了找到 匹配符号前的文本
     * 如果有匹配的符号,返回匹配符号之前的文本,否则就是没有匹配到,返回剩下的尾巴
     * 比如 这里是{{name}}的家  re为 {{  会返回 这里是
     */
    Scanner.prototype.scanUntil = function (re) {
        // 得到匹配符号在尾巴中的位置(尾巴会变化,所有位置也会在变化)
        var index = this.tail.search(re),
            match;
        switch (index) {
            // -1说明没有匹配到特殊符号,说明已经扫描完了。直接返回剩下的文本。比把尾巴置空
            case -1:
                match = this.tail;
                this.tail = "";
                break;
            // 特殊符号刚好在开始地方,那匹配符号前 就没有文本。匹配到的就是空字符串
            case 0:
                match = "";
                break;
            // 其他情况 直接获取匹配符号前的文本,并把尾巴向后移动到匹配的地方(这个时候后面会用scan函数跳过匹配符号)
            default:
                match = this.tail.substring(0, index);
                this.tail = this.tail.substring(index);
        }
        // 位置 需要加上匹配的长度 ,并返回匹配的文本 
        this.pos += match.length;
        return match;
    };










    /**
     * 
     * view 类 数据 比如 {{name: 'zs', age: 14}}
     * 把数据作为参数 实例化Context 。便于 创建新的数据 并且根据树结构查找数据
     */
    function Context(view, parentContext) {
        // view 如果不存在,默认设置为空对象
        this.view = view == null ? {} : view;
        // 缓存  对象  .
        this.cache = {
            '.': this.view
        };
        // 设置父元素为 传入的第二个参数
        this.parent = parentContext;
    }
    /**
     * 使用给定视图创建一个新上下文,并将此上下文作为父级。
     */
    Context.prototype.push = function (view) {
        return new Context(view, this);
    };
    /**
     * 返回此上下文中给定名称的值,如果此上下文视图中缺少该值,则遍历上下文层次结构。
     */
    Context.prototype.lookup = function (name) {
        // 首先拿到缓存
        var cache = this.cache;
        // 定义查找的内容
        var value;
        // 如果要查找的 内容在缓存中  赋值为value
        // 如果是循环 的情况,获取 .  这个符号,会直接返回 上面 有缓存对象
        if (name in cache) {
            value = cache[name];
        } else {
            // context 初始定义为 当前对象,下面可能会改变
            var context = this,
                names, index;
            // 循环遍历 context ,知道context不存在为止
            while (context) {
                // 如果要查找的是 a.b.c 类似 的数据
                if (name.indexOf('.') > 0) {
                    // 首先把 数据赋值为value  比如 { a: { b: c: 'haha' } }
                    value = context.view;
                    // names 把 a.b.c 拆分为 [a.b.c]
                    names = name.split('.');
                    // index 初始为0
                    index = 0;
                    //  循环 names 数组 ,并分别从value中查找到值,并重新赋值为value
                    //  比如上面最后会得到 value 为 haha
                    while (value != null && index < names.length)
                        value = value[names[index++]];

                } 
                // 如果 查找的关键字没有点  并且 数据源是对象。那么直接返回匹配的值
                else if (typeof context.view == 'object') {
                    value = context.view[name];
                }
                // 如果找到匹配值,那么终止循环
                if (value != null)
                    break;
                // 如果没有找到匹配的值,那么把context 上下文重新定义为 父 上下文,并重新循环查找
                context = context.parent;
            }
            cache[name] = value;
        }

        // 如果查找到的内容是 函数,然后执行到,赋值给value
        if (isFunction(value))
            value = value.call(this.view);

        // 返回找到到的内容
        return value;
    };












    /**
     * 提供解析模板为tokens 然后把tokens 转为 dom字符串
     * 根据tokens 转换为 字符串,并且缓存它
     */
    function Writer() {
        this.cache = {};
    }
    /**
     * 清空缓存
     */
    Writer.prototype.clearCache = function () {
        this.cache = {};
    };
    /**
     * 解析和缓存给定的“模板”,并返回从解析生成的令牌数组。
     */
    Writer.prototype.parse = function (template, tags) {
        // 拿到缓存
        var cache = this.cache;
        // 如果缓存中有,直接返回对应的tokens
        var tokens = cache[template];
        // 若果缓存中没有。那么调用 parseTemplate返回获取tokens并缓存起来
        if (tokens == null)
            tokens = cache[template] = parseTemplate(template, tags);
           
        return tokens;
    };
    /**
     * 渲染函数 
     * template 为模板
     * view 为数据
     * partials 为补充模板 可以为对象也可以为函数
     */
    Writer.prototype.render = function (template, view, partials) {
        // 把模板 转化 tokens
        var tokens = this.parse(template);
        // 数据 是不是 Context的实例,不是的话,用 Context实例化处理数据
        var context = (view instanceof Context) ? view : new Context(view);
        
        return this.renderTokens(tokens, context, partials, template);
    };
    /**
     * 递归函数  用于处理tokens  处理为dom字符串
     *     
     *  */
    Writer.prototype.renderTokens = function (tokens, context, partials, originalTemplate) {
        var buffer = ''; // 初始为字符串
        
        var self = this;
        // 附属渲染  获取  顶层渲染的数据
        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]) {
                // 如果是# 那这个token 就是循环的token 
                // 比如 ["#", "list", 194, 203, Array(5), 260]
                case '#':
                    // 数据中查找 list 对应的 数据
                    value = context.lookup(token[1]);
                    // 如果数据没有则跳过
                    if (!value)
                        continue;
                    // 如果是 数组
                    // 比如  {list: [{name:'zs'},{name: 'ls'}]}
                    // value 为[{name:'zs'},{name: 'ls'}]
                    if (isArray(value)) {
                        for (var j = 0, valueLength = value.length; j < valueLength; ++j) {

                            // 递归 把 Array(5) 作为token, 每一项 {name: 'xx'} 作为数据
                            buffer += this.renderTokens(token[4], context.push(value[j]), partials,
                                originalTemplate);
                        }
                        // 如果 找到的 value 为对象或字符串
                        // {list: {name:'zs', age:14}} 或者 {list: 'abcd'}
                        // value 为 {name:'zs', age:14} 或 'abcd'
                    } else if (typeof value === 'object' || typeof value === 'string') {
                        // 也是递归 把 Array(5)作为token, 直接把当前value作为数据
                        buffer += this.renderTokens(token[4], context.push(value), partials,
                            originalTemplate);
                        // 如果找到的函数
                        /**
                         *  arr: ()=> (template, fn)=>{
                         *     return fn(template)
                         *   }
                         * 
                         *  template  == originalTemplate.slice(token[3], token[5]) 为循环内的位解析字符串
                         * fn == subRender 用根 数据渲染当前模板
                         */
                    } else if (isFunction(value)) {
                        // 如果 原始模板不是字符串 则报错
                        if (typeof originalTemplate !== 'string')
                            throw new Error(
                                'Cannot use higher-order sections without the original template');
                        // 返回一个自定义函数, 原函数匹配的字符串为第一个参数 ,subRender为第二个参数(以根数据来渲染当前模板)
                        value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
                        
                        if (value != null)
                            buffer += value;
                    } else {
                        // 其他情况 直接调用 递归处理 并且不用 context.push反正创建子context
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    }
                    break;
                case '^':
                    // 查找到 数据中的对应值 (lookup方法会找到最上层)
                    value = context.lookup(token[1]);
                    // 如果没有找到值 或者 是一个空数组  调用 renderTokens 方法查找对应数据
                    
                    if (!value || (isArray(value) && value.length === 0))
                        buffer += this.renderTokens(token[4], context, partials, originalTemplate);
                    break;
                case '>':
                    // > 如果没有 分部 partials 跳过
                    if (!partials)
                        continue;
                        // 判断partials 是不是函数,函数执行 否则世界查找partials的 值
                    value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
                    if (value != null)
                         // 有值的话继续递归
                        buffer += this.renderTokens(this.parse(value), context, partials, value);
                    break;
                case '&':
                    // 如果是& 说明是{{{}}} 中的数据, 去context 中查找到对应的数据 后直接拼接
                    value = context.lookup(token[1]);
                    if (value != null)
                        buffer += value;
                    break;
                case 'name':
                    // name  去context 中查找到对应的数据
                    value = context.lookup(token[1]);
                    // 如果不为空,转义之后拼接
                    if (value != null)
                        buffer += mustache.escape(value);
                    break;
                case 'text':
                    // text 是文本 ,直接拼接
                    buffer += token[1];
                    break;
            }
        }
        return buffer;
    };

}));

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

推荐阅读更多精彩内容