什么是模板引擎?

模板引擎是为了使用户界面与业务数据(内容)拆散而产生的,它能够生成特定格局的文档,用于网站的模板引擎就会生成一个规范的文档,就是将模板文件和数据通过模板引擎生成一个HTML代码。

本篇内容须要的js常量及dom构造
<body>    <ul id="list"></ul>    <script>        var arr = [            {name: '小明', age:23},            {name: '小红', age:25},            {name: '小强', age: 27}        ]    </script></body>

模板引擎的倒退

  1. 纯dom法-创立节点法
var list = document.getElementById('list')for (var i = 0; i < arr.length; i++) {    // 每遍历一项,都要用DOM办法创立li标签    var oli = document.createElement('li');    var hdDiv = document.createElement('div');    hdDiv.className = 'hd';    hdDiv.innerText = arr[i].name + '根本信息';    var dbDiv = document.createElement('div');    dbDiv.className = 'db';    dbDiv.innerText = arr[i].name + '的根本信息';    var p1 = document.createElement('p')    p1.innerText = '姓名' + arr[i].name    // 创立的节点是孤儿节点,必须上树能力被用户看见    dbDiv.appendChild(p1)    oli.appendChild(hdDiv)    oli.appendChild(dbDiv)    list.appendChild(oli)}

这种形式内存开销大,繁冗简短。

  1. 数组join()法-以字符串的视角追加内容
var list = document.getElementById('list')for (let i = 0;i < arr.length; i++) {    list.innerHTML += [        '<li>',        '    <div class="hd">'+arr[i].name+'的信息</div>',        '    <div class="bd">',        '        <p>姓名:'+arr[i].name+'</p>',        '        <p>年龄:'+arr[i].age+'</p>',        '        <p>性别:</p>',        '    </div>',        '</li>'    ].join('');}
  1. ES6反引号法-字符串自身能够换行,缩小了短字符串的个数
var list = document.getElementById('list')for (let i = 0;i < arr.length; i++) {   list.innerHTML += `       <li>           <div class="hd">${arr[i].name}的信息</div>           <div class="bd">               <p>姓名:${arr[i].name}</p>               <p>年龄:${arr[i].age}</p>               <p>性别:</p>           </div>       </li>   `}
  1. mustache模板引擎
var templateStr = `    {{#arr}}    <li>        <div class="hd">{{name}}的信息</div>        <div class="bd">            <p>姓名:{{name}}</p>            <p>年龄:{{age}}</p>            <p>性别:</p>        </div>    </li>   {{/arr}}`;// render接管两个参数:1.模板字符串templateStr;2.数据datavar domStr = Mustache.render(templateStr, data); // 最初生成dom字符串domStr// console.log(domStr)var container = document.getElementById('list');container.innerHTML = domStr;

mustache模板字符串实现思路

Mustache的底层外围机理tokens:js的嵌套数组(模板字符串的js示意模式),且Tokens 是“形象语法树”、“虚构节点”等等的思路起源。
Mustache外围机理

stateDiagram-v2    模板字符串 --> tokens    tokens --> Dom字符串   数据 --> Dom字符串        

一个tokens如下:

[  ["text", "<h1>我买了一个"],  ["name", "thing"],  ["text", ",好"],  ["name", "mood"],  ["text", "啊</h1>"]]

这个二维数组的每一项就是一个token。

mustache具体实现思路如下:

  1. 筹备好模板字符串与数据,定义render渲染函数,传入模板字符串与数据,返回编译好的Dom字符串:
var htmlStr = render(templateStr, data)document.getElementById('main').innerHTML = htmlStr

在render渲染函数外部,把模板字符串编译成tokens数组,再把tokens编译成Dom字符串:

render(templateStr, data) {    // 调用parseTempToToken函数,让模板字符串变成tokens数组    var tokens = parseTempToToken(templateStr);    // 调用renderTemplate函数,让tokens数组变为dom字符串    var domStr = renderTemplate(tokens, data)    console.log('domStr:\n', domStr)    return domStr;}
  1. 定义parseTempToToken,将模板字符串变为tokens数组

    /** * 将模板字符串变为tokens数组*/export default function parseTempToToken (templateStr) { let tokens = []; // 创立扫描器 let scanner = new Scanner(templateStr); let words; // 让扫描器工作 while (!scanner.eos()) {     // 收集开始标记呈现之前的文字     words = scanner.scanUtil('{{');     if (words !== '') {         // 尝试写一下去掉空格,智能判断是一般文字的空格,还是标签中的空格         // 标签中的空格不能去掉比方<div class="box">不能去掉class后面的空格         let isInJJH = false;         // 空白字符串         var _words = '';         for (let i = 0; i < words.length; i++) {             // 判断是否在标签内             if (words[i] === '<') {                 isInJJH = true             } else if (words[i] === '>') {                 isInJJH = false             }             if (!/\s/.test(words[i])) {                 // 如果这项不是空格,拼接上                 _words += words[i]             } else {                 // 如果这项是空格,只有在标签里才保留空格                 if(isInJJH) {                     _words += words[i]                 }             }         }         // 存起来,去掉空格         tokens.push(['text', _words]);     }     // 过双括号{{     scanner.scan('{{')     // 收集开始标记呈现之前的文字     words = scanner.scanUtil('}}');     if (words !== '') {         // 这个words就是{{}}两头的内容,判断一下首字符         if (words[0] === '#') {             // 存起来,从下标为1的项开始存,因为下标为0的项是#             tokens.push(['#', words.substring(1)])         } else if (words[0] === '/') {             // 存起来,从下标为1的项开始存,因为下标为0的项是/             tokens.push(['/', words.substring(1)])         } else {             // 存起来             tokens.push(['name', words]);         }         // 存起来         // tokens.push(['name', words]);     }     // 过双括号{{     scanner.scan('}}') } // 返回折叠的tokens return nestTokens(tokens);}

    1)定义扫描器,次要体现在对模板字符串遍历时,指针的挪动

    /** * 扫描器类*/export default class Scanner { constructor(templateStr) {     // 将模板字符串写到实例上     this.templateStr = templateStr;     // 指针     this.pos = 0;     // 尾巴,一开始就是模板字符串原文     this.tail = templateStr; } // 性能弱,就是走过指定内容,没有返回值 scan(tag) {     if (this.tail.indexOf(tag) === 0) {         // tag 有多长,比方{{长度是2,就让指针后移多少位         this.pos += tag.length;         // 扭转尾巴为从以后指针这个字符开始,到最初的全副字符         this.tail = this.templateStr.substring(this.pos)     } } // 让指针进行扫描,直到遇见指定内容完结,并且可能返回完结之前路过的文字 scanUtil(stopTag) {     // 记录一下执行本办法时候的pos的值     const pos_backup = this.pos;     // 当尾巴的结尾不是stopTag的时候,就阐明还没扫描到stopTag     // && 避免找不到,寻找到最初也要停止下来     while(!this.eos() && this.tail.indexOf(stopTag) !== 0) {         this.pos ++;         // 扭转尾巴为从以后指针这个字符开始,到最初的全副字符         this.tail = this.templateStr.substring(this.pos)     }     return this.templateStr.substring(pos_backup, this.pos) } // 指针是否曾经到头,返回布尔值 eos() {     return this.tail === ''; }}

    parseTempToToken函数中调用扫描器scanner类的scanUtil办法(收集开始标记之前的文字)、scan办法(过滤双括号{{,}}),eos办法(判断是否宰割到字符尾部),最初返回宰割好的平级tokens。
    2)定义nestTokens函数,将上一步扫描完生成的平级tokens解决为嵌套tokens

/** * 函数的性能是折叠tokens,将#和/之间的tokens可能整合起来,作为它的下标为3的项*/export default function nestTokens(tokens) {    // 后果数组    let nestTokens = [];    // 栈构造,寄存小tokens,栈顶(凑近端口的,最新进入的)的tokens数组中以后操作的这个tokens小数组    var sections = [];    // 收集器,初始指向nestTokens,援用类型值,所以指向的是同一个数组    // 收集器的指向会变动,当遇见#的时候,收集器会指向这个token的下标为2的新数组    let collector = nestTokens;    for(let i = 0; i < tokens.length; i++) {        let token = tokens[i];        switch(token[0]) {            case '#':                // 收集器中放入这个token                collector.push(token);                // 入栈                sections.push(token);                // 收集器要换内容, 给token增加下标为2的项,并让收集器指向它                collector = token[2] = [];                break;            case '/':                // 出栈,pop()会返回刚刚弹出的项                let section_pop = sections.pop();                // 扭转收集器为栈构造队尾(队尾是栈顶)那项的下标为2的数组                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens;                break;           default:               collector.push(token)               break;        }    }    return nestTokens;}

在nestTokens 通过设置收集器collector,并在循环中判断token字符中是否有“#”来判断以后级有没有下一级,有的话将收集器指向本级的下一级,通过‘/’判断本机循环完结并出栈;如果都没有那就是本级的平级,压入栈顶。
至此,模板字符串的tokens组装结束


  1. 定义renderTemplate函数,将tokens注入数据(把tokens数组变为dom字符串)
/** * 函数的性能是让tokens数组变为dom字符串*/export default function renderTemplate(tokens, data) {    // 后果字符串    var resultStr = '';    // 遍历tokens    for (let i = 0; i < tokens.length; i++) {        let token = tokens[i];        // 看类型        if (token[0] === 'text') {            resultStr += token[1]        } else if (token[0] === 'name') {            // 如果是name类型,那么就间接应用它的值,当然要用lookup            // 避免这里是'a.b.b'有逗号的模式            resultStr += lookup(data, token[1])        } else if (token[0] === '#') {            resultStr += parseArray(token, data)        }    }    return resultStr}
  1. renderTemplate中调用lookup函数,在dataObj对象中,寻找用间断点符号的keyName属性,也就是读取嵌套的简单数据类型中的值。
/** * 性能是能够在dataObj对象中,寻找用间断点符号的keyName属性,比方,dataObj 是 * { *  a: { *      b: { *        c: 100 *      } *  } * } * 那么lookup(dataObj, 'a.b.b') 后果就是100*/export default function lookup(dataObj, keyName) {    // 看看keyName 中有没有点符号    if (keyName.indexOf('.') > -1 && keyName !== '.') {        var keys = keyName.split('.');        // 设置一个长期变量,这个长期变量用于周转,一层一层找上来        var temp = dataObj;        for (let i = 0; i < keys.length; i++) {            // 每找一层,都把它设为新的长期变量            temp = temp[keys[i]]        }        return temp;    }    // 如果没有点符号    return dataObj[keyName];}
  1. 在曾经确定有上级的token中递归调用parseArray()办法,遍历数据,再在renderTemplate中填充。
/** * 解决数组,联合renderTemplate实现递归 * 这个函数收的参数是token!而不是tokens! * token 就是一个简略的['#', 'student', []] * 这个函数要递归调用renderTemplate函数,调用多少次由data决定 * 比方data的模式是这样的: * {        students: [            {name: '小明', age: 12, hobbies: ['游泳', '羽毛球']},            {name: '小红', hobbies: ['足球', '篮球', '羽毛球']}        ]    }    那么parseArray()函数就要递归调用renderTemplate()三次,数组长度是3*/export default function parseArray(token, data) {    // 失去整体数据data中这个数组要应用的局部    var v = lookup(data, token[1]);    // 后果字符串    var resultStr = '';    // 留神,上面这个循环可能是整个包中最难思考的一个循环    // 它是遍历数据,而不是遍历tokens    for (let i = 0; i < v.length; i++) {        // 这里须要补一个“.”属性        resultStr += renderTemplate(token[2], {            ...v[i],            '.': v[i]        })    }    return resultStr;}

到此mustache实现模板字符串的整个过程就全副完结了,回到最后,渲染函数render里边的失去的tokens,domStr如下图:

渲染到页面上: