Vue源码-mustache模板引擎

337 阅读5分钟

mustache 模板引擎

模板引擎是将数据变成试图的最优雅的解决方

历史上出现的数据变成视图的方法

  1. 纯 DOM 方法
  2. 数组 join 法
  3. ES6 的反引号法
  4. 模板引擎

1. 纯 DOM 方法

需要的节点全靠生成后上树, 节点属性, 相当的复杂

<body>
  <ul id="list"></ul>
  <script>
    const arr = [
      { name: "123", age: 123 },
      { name: "456", age: 456 },
      { name: "789", age: 789 },
    ];
    const list = document.getElementById("list");
    for (let index = 0; index < arr.length; index++) {
      const element = arr[index];
      //   需要的节点全靠生成后上树,相当的复杂
      let oli = document.createElement("li");
      oli.innerText = element.name;
      let oli = document.createElement("li");
      oli.innerText = element.age;
      //   上树
      list.appendChild(oli);
    }
  </script>
</body>

2. 数组的 join 方法

使用了数组的有结构的形式 数组的 join 出来字符串 ['A','B'].join('') ---> 'AB'

const arr = [
  { name: "123", age: 123 },
  { name: "456", age: 456 },
  { name: "789", age: 789 },
];
const list = document.getElementById("list");
for (let index = 0; index < arr.length; index++) {
  const element = arr[index];
  list.innerHTML += [
    "<li>名字: " + element.name + "</li>",
    "<li>年龄: " + element.age + "</li>",
  ].join("");
}

3.ES6 的反引号法

const arr = [
  { name: "123", age: 123 },
  { name: "456", age: 456 },
  { name: "789", age: 789 },
];
const list = document.getElementById("list");
for (let index = 0; index < arr.length; index++) {
  const element = arr[index];
  list.innerHTML += `
            <li>名字:${element.name}</li>
            <li>年龄:${element.age}</li>
            `;
}

4. 模板引擎

mustache 基本使用

循环

var templateStr = `
        <ul>
            {{#arr}}
                <li>名字:{{name}}</li>
                <li>年龄:{{age}}</li>
            {{/arr}}
        </ul>
      `;
var data = {
  arr: [
    { name: "123", age: 123 },
    { name: "456", age: 456 },
    { name: "789", age: 789 },
  ],
};
const domStr = Mustache.render(templateStr, data);
console.log(domStr);
//   <ul>
//         <li>名字:123</li>
//         <li>年龄:123</li>
//         <li>名字:456</li>
//         <li>年龄:456</li>
//         <li>名字:789</li>
//         <li>年龄:789</li>
// </ul>
list.innerHTML += domStr;

不循环

var templateStr = `
                  <li>名字:{{name}}</li>
                  <li>年龄:{{age}}</li>
        `;
var data = { name: "123", age: 123 };
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;

循环简单数组

.就可以表示项本身

var templateStr = `
          <ul>
              {{#arr}}
                  <li>名字:{{.}}</li>
              {{/arr}}
          </ul>
        `;
var data = {
  arr: [1, 2, 3, 4, 5, 6],
};
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;

数组的嵌套

var templateStr = `
          <ul>
              {{#arr}}
                    <li>名字:{{name}}</li>
                    <li>年龄:{{age}}</li>
                    <div>
                        {{#aihao}}
                        爱好: {{.}}
                        {{/aihao}}
                    <div/>
              {{/arr}}
          </ul>
        `;
var data = {
  arr: [
    { name: "123", age: 123, aihao: ["吃饭1", "打球1"] },
    { name: "456", age: 456, aihao: ["吃饭2", "打球2"] },
    { name: "789", age: 789, aihao: ["吃饭3", "打球3"] },
  ],
};
const domStr = Mustache.render(templateStr, data);
// 名字:123
// 年龄:123
// 爱好: 吃饭1 爱好: 打球1
// 名字:456
// 年龄:456
// 爱好: 吃饭2 爱好: 打球2
// 名字:789
// 年龄:789
// 爱好: 吃饭3 爱好: 打球3
list.innerHTML += domStr;

使用布尔值

var templateStr = `
        {{#m}}
        <li>名字:1</li>
        {{/m}}
          `;
var data = { m: true }; // true就显示,false就不显示
const domStr = Mustache.render(templateStr, data);
list.innerHTML += domStr;

早期还没有反引号的神操作

<script type="text/template" id="myTemplate">
  <ul>
      {{#arr}}
            <li>名字:{{name}}</li>
            <li>年龄:{{age}}</li>
            <div>
                {{#aihao}}
                爱好: {{.}}
                {{/aihao}}
            <div/>
      {{/arr}}
  </ul>
</script>

<!-- 神人啊,这都能想得到? -->
<script>
  var templateStr = document.getElementById("myTemplate").innerHTML;
  var data = {
    arr: [
      { name: "123", age: 123, aihao: ["吃饭1", "打球1"] },
      { name: "456", age: 456, aihao: ["吃饭2", "打球2"] },
      { name: "789", age: 789, aihao: ["吃饭3", "打球3"] },
    ],
  };
  const domStr = Mustache.render(templateStr, data);
  // 名字:123
  // 年龄:123
  // 爱好: 吃饭1 爱好: 打球1
  // 名字:456
  // 年龄:456
  // 爱好: 吃饭2 爱好: 打球2
  // 名字:789
  // 年龄:789
  // 爱好: 吃饭3 爱好: 打球3
  list.innerHTML += domStr;
</script>

mustache 底层核心机理

mustache 不能用简单的正则表达式思路实现,在复杂模板字符串就不支持了

只适合最简单的

var templateStr = "<li>名字:{{name}}</li>";
var data = { name: "2123" };
const newStr = templateStr.replace(/\{\{(\w+)\}\}/g, function (str, key) {
  console.log(str, key); // {{name}} name
  return data[key];
});
list.innerHTML += newStr;
模板字符串--编译-->tokens--解析(结合数据)-->dom字符串

tokens

就是一个 js 的嵌套数据,也就是模板字符串的 js 表示

他是 抽象语法树 虚拟节点 等等的开山鼻祖

每次的 {{ }} 都是一个分割,都分为二

单层模板字符串转 tokens

<h1>我买了一个 {{ thing }},好{{ mood }}啊</h1>
[
    ['text':'<h1>我买了一个'],   // token
    ['name':'thing'],           // token
    ['text':'好'],              // token
    ['name':'mood'],            // token
    ['text':'啊</h1>']          // token
]

多层模板字符串转 tokens

<div>
  <ul>
    {{#arr}}
    <li>{{.}}</li>
    {{/arr}}
  </ul>
</div>

以此类推,更多层时候逐层处理即可

[
  ["text", "<div><ul>"],
  [
    "#",
    "arr",
    [
      ["text", "<li>"],
      ["name", "."],
      ["text", "</li>"],
    ],
  ],
  ["text", "</ul></div>"],
];

源码 tokens

// 这里就是返回的tokens
return nestTokens(squashTokens(tokens));

手写实现 mustache 库

生成库是 UMD,这样意味着他同事可以在 nodejs 环境中使用,也可以在浏览器环境当中使用,实现 UMD 不难,只需要一个"通用头"即可

后进先出,先进后出, 在我们这里,遇到#旧进栈,遇到/旧出栈

[
  ["name", "\n        <div>\n      <ol>\n        "],
  ["#", "students"],
  ["name", "\n        <li>\n          学生"],
  ["text", "item.name"],
  ["name", "的爱好是\n          <ol>\n            "],
  ["#", "item.hobbies"],
  ["name", "\n            <li>{.}</li>\n            "],
  ["/", "item.hobbies"],
  ["name", "\n          </ol>\n        </li>\n        "],
  ["/", "students"],
  ["name", "\n      </ol>\n    </div>\n        "],
];

手写简易源码

index.js

import parseTemplateToTokens from "./parseTemplateToTokens.js";
import renderTemplate from "./renderTemplate.js";
window.LGQ_tem = {
  // 渲染方法
  render(templateStr, data) {
    // 调用 parseTemplateToTokens 来处理模板字符串 位 tokens 数组
    var tokens = parseTemplateToTokens(templateStr);
    // 调用renderTemplate函数,让tokens数组变成dom字符串
    var domStr = renderTemplate(tokens, data);
    return domStr;
  },
};

parseTemplateToTokens.js

import Scanner from "./Scanner.js";
import nextTokens from "./nextTokens.js";
// 将模板字符串变成tokens、 数组
export default function parseTemplateToTokens(templateStr) {
  var tokens = [];
  var scanner = new Scanner(templateStr);
  var words;
  // 当scan没有当头就循环持续工作
  while (scanner.eos()) {
    // 收集开始标记出现之前的文字
    words = scanner.scanUtil("{{");
    // 这个words就是{{}}中间的东西,我们要判断下,如果首字符是#号
    if (words != "") {
      // 存起来
      // 这里需要处理空格,文字之间的空格去除,但是标签上的不能去掉,这里是暴力去掉了所有
      // 尝试写一下去掉空格,智能判断是文字之间的空格,还是标签上的不能去掉
      let isInJJh = false;
      var _words = "";
      for (let i = 0; i < words.length; i++) {
        const element = words[i];
        if (element === "<") {
          isInJJh = true;
        } else if (element == ">") {
          isInJJh = false;
        }
        if (!/\s/.test(element)) {
          _words += element;
        } else {
          // 如果是空格,只有在标签内才拼接
          if (isInJJh) {
            // 不在标签内
            _words += element;
          }
        }
      }
      tokens.push(["text", _words]);
    }

    // 过双大括号
    scanner.scan("{{");
    // 收集开始标记出现之前的文字
    // 存起来 这个变量需要特殊处理,情况多
    words = scanner.scanUtil("}}");
    if (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]);
      }
    }
    // 过双大括号
    scanner.scan("}}");
  }
  // 返回折叠收集的tokens
  return nextTokens(tokens);
}

renderTemplate.js

import loopup from "./lookup.js";
import parseArray from "./parseArray.js";
// 函数的功能是让tokens数组变成dom字符串
export default function renderTemplate(tokens, data) {
  // 结果字符串
  var resultStr = "";
  // 遍历tokens
  for (let index = 0; index < tokens.length; index++) {
    const token = tokens[index];
    // 看类型
    if (token[0] === "text") {
      resultStr += token[1];
    } else if (token[0] === "name") {
      //   这样 data[token[1]] 不能处理 {{a.b.c}}的情况, data[a.b.c]js不认识的
      //   所以需要自己写个lookup方法来单独处理数据
      resultStr += loopup(data, token[1]);
    } else if (token[0] == "#") {
      resultStr += parseArray(token, data);
    }
  }
  return resultStr;
}

Scanner.js

export default class Scanner {
  //   官方对于这两个方法的定义是;
  //   scanUtil方法是用于 处理 {{ 和 }} 之外匹配的内容
  //   sacn方法妗妗是处理{{ 和 }} 这两个符号
  //   "我买了一个  {{  thing        }}    ,好{{mood}}啊";
  //   --scanUtil--scan--scanUtil--scan--scanUtil
  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;
    // 向右走字符,也就是pos+,但是也不能一只移动,就需要一个尾巴(可以检查), 不能一直往下走
    while (this.tail.indexOf(stopTag) != 0 && this.eos()) {
      this.pos++;
      //   pos移动,那么也需要尾巴跟着动,尾巴包括当前指针这一位的后面的所有
      this.tail = this.templateStr.substring(this.pos);
    }
    // 返回的不能包括当前指针位
    return this.templateStr.substring(pos_backup, this.pos);
  }
  //   指针是否到头,返回布尔值
  eos() {
    return this.pos < this.templateStr.length;
  }
}

nextTokens.js

// 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为3的项
export default function nextTokens(tokens) {
  // 结果数组
  var nestedTokens = [];
  //   栈结构,栈顶,(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
  var sections = [];
  //   收集器(天生指向nestedTokens结果数组,引用数据类型,所以指向的是同一个数组)
  var collector = nestedTokens; //收集器的指向会变化,当遇到#号的时候,指向这个下标为2的新数组,

  for (let index = 0; index < tokens.length; index++) {
    const token = tokens[index];
    switch (token[0]) {
      case "#":
        // 往收集器中放入token
        collector.push(token);
        // 入栈
        sections.push(token);
        // 收集器要换人,给收集器指向当前子数组里
        collector = token[2] = [];
        break;
      case "/":
        // 出栈 pop会返回刚刚弹出的项
        sections.pop();
        // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组, 就是向为目前结构的上一层
        // 如果栈里面有东西,那就回第二项,但是没有的话,就回到最顶层了
        collector =
          sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
        break;
      default:
        // 不要管当前的 collector 是什么,可能是最顶层,也可能是子项,只要往里面推就行
        collector.push(token);
        break;
    }
  }
  //   这个先进去的#肯定是后面才/出来   (写法很乱很乱)
  //   for (let index = 0; index < tokens.length; index++) {
  //     const token = tokens[index];
  //     switch (token[0]) {
  //       // 入栈(压栈)
  //       case "#":
  //         // 给这个token下标为2的项创建一个数组,收集他们的子元素
  //         token[2] = [];
  //         sections.push(token);
  //         nestedTokens.push(token);
  //         break;
  //       // 出栈(弹栈)
  //       case "/":
  //         // pop会返回刚才弹出的项
  //         let sectionPop = sections.pop();
  //         sections;
  //         // 刚刚弹出的项还没有加到结果当中
  //         nestedTokens.push(sectionPop);
  //         break;
  //       default:
  //         // 判断栈这个队列当前情况,如果是空的,我就往结尾放,
  //         if (sections.length == 0) {
  //           // 说明这个时候还没有遇到任何#,或者已经出去了
  //           nestedTokens.push(token);
  //         } else {
  //           // 往这个子项的数组里面加
  //           sections[sections.length - 1][2].push(token);
  //         }
  //     }
  //   }
  return nestedTokens;
}

lookup.js

// 函数的功能是可以在 dataObj 对象之中,寻找用连续点符号对应的数据
// 例如
// dataObj = {
//     a:{
//         b:{
//             c:100
//         }
//     }
// }
// 那么
// loopup(dataObj, 'a.b.c') 返回 100
export default function loopup(dataObj, keyName) {
  // 自己写的方法
  //   根据.来进行分割,但是还不能是. (因为是.的时候就是循环简单的单层数组数据)
  if (keyName.indexOf(".") != -1 && keyName !== ".") {
    const arr = keyName.split(".");
    //   临时变量用于一层一层往下找
    let tem = dataObj;
    if (arr.length > 1) {
      arr.forEach((element) => {
        // 在剩余的tem里面一层一层找
        tem = tem[element];
      });
      console.log("dataObj", dataObj);
      return tem;
    }
  }

  return dataObj[keyName];
}

parseArray.js

import loopup from "./lookup.js";
import renderTemplate from "./renderTemplate.js";
/**
 * 处理数据,结合 renderTemplate实现递归
 * 注意: 这个函数的参数是token, 而不是 tokens;
 * token是什么, 就是一个简单的[("#", "student", [])];
 * 
 * 这个函数递归调用 renderTemplate 函数,调用多少次?
 * 调用的次数由data决定
 * 比如data的形式是这样的
 * {
        students: [
        { name: "小明", hobbies: ["游泳", "健身"] },
        { name: "小红", hobbies: ["足球", "篮球", "羽毛球"] },
        { name: "小强", hobbies: ["吃饭", "睡觉"] },
        ],
    }
    那么我们这个函数 parseArray 要调用 renderTemplate 函数三次,因为数组的长度是3
 * @param {*} token 
 * @param {*} data 
 */
export default function parseArray(token, data) {
  //   得到data中整体的数据
  var v = loopup(data, token[1]);
  //   结果字符串
  var resultStr = "";
  //   遍历v,不一定是数组,因为#后面也可以跟布尔值
  //   这里是遍历数据的,数据有几个就需要渲染tokens几次
  for (let index = 0; index < v.length; index++) {
    // 这个需要补一个.属性的识别
    resultStr += renderTemplate(token[2], {
      // 补充一个.属性
      ".": v[index],
      // 相当于这个数据的小对象,是v[index]的展开,就是v[index]本身
      ...v[index],
    });
  }
  return resultStr;
}