经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » JavaScript » 查看文章
Vue源码-手写mustache源码
来源:cnblogs  作者:会飞的一棵树  时间:2021/5/24 10:52:11  对本文有异议

引言

在Vue中使用模板语法能够非常方便的将数据绑定到视图中,使得在开发中可以更好的聚焦到业务逻辑的开发。

mustache是一个很经典且优秀的模板引擎,vue中的模板引擎也对其有参考借鉴,了解它能更好的知道vue的模板引擎实现的原理。

数据转换为视图的方案

Vue的核心之一就是数据驱动,而模板引擎就是实现数据驱动上的很重要一环。借助模板引擎能够方便的将数据转换为视图,那么常用转换的方案有哪些呢。

  1. 纯 DOM 法,使用 JS 操作 DOM,创建和新增 DOM 将数据放在视图中。(直接干脆,但在处理复杂数据时比较吃力)
  2. 数组 Join 法,利用数组可以换行写的特性,[].jion('')成字符传,再使用 innerHTML。(能保证模板的可读和可维护性)
  1. <div id="container"></div>
  2. <script>
  3. // 数据
  4. const data = { name: "Tina", age: 11, sex: "girl"};
  5. // 视图
  6. let templateArr = [
  7. " <div>",
  8. " <div>" + data.name + "<b> infomation</b>:</div>",
  9. " <ul>",
  10. " <li>name:" + data.name + "</li>",
  11. " <li>sex:" + data.sex + "</li>",
  12. " <li>age:" + data.age + "</li>",
  13. " </ul>",
  14. " </div>",
  15. ];
  16. // jion成domStr
  17. let domStr = templateArr.join('');
  18. let container = document.getElementById('container');
  19. container.innerHTML = domStr;
  20. </script>
  1. ES6 的模板字符串。
  2. 模板引擎。

mustache使用示例

  1. <!-- 引入mustache -->
  2. <script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
  3. <div class="container"></div>
  4. <script>
  5. // 模板
  6. var templateStr = `
  7. <ul>
  8. {{#arr}}
  9. <li>
  10. <div class="hd">{{name}}<b> infomation</b></div>
  11. <div class="bd">
  12. <p>name:{{name}}</p>
  13. <p>sex:{{sex}}</p>
  14. <p>age:{{age}}</p>
  15. </div>
  16. </li>
  17. {{/arr}}
  18. </ul>`;
  19. // 数据
  20. var data = {
  21. arr: [
  22. { name: "Tina", age: 11, sex: "female", friends: ["Cate", "Mark"] },
  23. { name: "Bob", age: 12, sex: "male", friends: ["Tim", "Apollo"] },
  24. { name: "Lucy", age: 13, sex: "female", friends: ["Bill"] },
  25. ],
  26. };
  27. // 使用render方法生成绑定了数据的视图的DOM字符串
  28. var domStr = Mustache.render(templateStr, data);
  29. // 将domStr放在contianer中
  30. var container = document.querySelector(".container");
  31. container.innerHTML = domStr;
  32. </script>

mustache实现原理

mustache会将模板转换为tokens,然将tokens和数据相结合,再生成dom字符串。tokens将模板字符串按不同类型进行拆分后封装成数组,其保存的是字符串对应的mustache识别信息。
image
模板:

  1. <ul>
  2. {{#arr}}
  3. <li>
  4. <div class="hd">{{name}}<b> infomation</b></div>
  5. <div class="bd">
  6. <p>sex:{{sex}}</p>
  7. <p>age:{{age}}</p>
  8. </div>
  9. </li>
  10. {{/arr}}
  11. </ul>

其转换的tokens(为排版方便清除部分空字符),这里tokens中的数字是,字符的开始和结束位置。

  1. [
  2. ["text", "? <ul>?", 0, 12],
  3. ["#", "arr", 22, 30, [
  4. ["text", "<li>?<div class="hd">", 31, 78],
  5. ["name", "name", 78, 86],
  6. ["text", "<b> infomation</b></div>?<div class="bd">?? <p>sex:", 86, 166],
  7. ["name", "sex", 166, 173],
  8. ["text", "</p>?<p>age:", 173, 201],
  9. ["name", "age", 201, 208],
  10. ["text", "</p>?</div>?</li>?", 208, 252]
  11. ], 262
  12. ],
  13. ["text", "</ul>?", 271, 289]
  14. ]

数据:

  1. {
  2. arr: [
  3. { name: "Tina", age: 11, sex: "female" },
  4. { name: "Bob", age: 12, sex: "male" }
  5. ]
  6. }

生成后的Dom的字符串:

  1. <ul>
  2. <li>
  3. <div class="hd">Tina<b> infomation</b></div>
  4. <div class="bd">
  5. <p>sex:female</p>
  6. <p>age:11</p>
  7. </div>
  8. </li>
  9. <li>
  10. <div class="hd">Bob<b> infomation</b></div>
  11. <div class="bd">
  12. <p>sex:male</p>
  13. <p>age:12</p>
  14. </div>
  15. </li>
  16. </ul>

mustache关键源码

扫描模板字符串的Scanner

扫描器有两个主要方法。Scanner扫描器接收模板字符串作其构造的参数。在mustache中是以{{}}作为标记的。
scan方法,扫描到标记就将指针移位,跳过标记。
scanUntil方法是会一直扫描模板字符串直到遇到标记,并将所扫描经过的内容进行返回。

  1. export default class Scanner {
  2. constructor(templateStr) {
  3. // 将templateStr赋值到实例上
  4. this.templateStr = templateStr;
  5. // 指针
  6. this.pos = 0;
  7. // 尾巴字符串,从指针位置到字符结束
  8. this.tail = templateStr;
  9. }
  10. // 扫描标记并跳过,没有返回
  11. scan(tag) {
  12. if (this.tail.indexOf(tag) === 0) {
  13. // 指针跳过标记的长度
  14. this.pos += tag.length;
  15. this.tail = this.templateStr.substring(this.pos);
  16. }
  17. }
  18. // 让指针进行扫描,直到遇见结束标记,并返回扫描到的字符
  19. // 指针从0开始,到找到标记结束,结束位置为标记的第一位置
  20. scanUntil(tag) {
  21. const pos_backup = this.pos;
  22. while (!this.eos() && this.tail.indexOf(tag) !== 0) {
  23. this.pos++;
  24. // 跟新尾巴字符串
  25. this.tail = this.templateStr.substring(this.pos)
  26. }
  27. return this.templateStr.substring(pos_backup, this.pos);
  28. }
  29. // 判断指针是否到头 true结束
  30. eos() {
  31. return this.pos >= this.templateStr.length;
  32. }
  33. }

将模板转换为tokens的parseTemplateToTokens

  1. export default function parseTemplateToTokens(templateStr) {
  2. const startTag = "{{";
  3. const endTag = "}}";
  4. let tokens = [];
  5. // 创建扫描器
  6. let scanner = new Scanner(templateStr);
  7. let word;
  8. while (!scanner.eos()) {
  9. word = scanner.scanUntil(startTag);
  10. if (word !== '') {
  11. tokens.push(["text", word]);
  12. }
  13. scanner.scan(startTag);
  14. word = scanner.scanUntil(endTag);
  15. // 判断扫描到的字是否是空
  16. if (word !== '') {
  17. if (word[0] === '#') {
  18. // 判断{{}}之间的首字符是否为#
  19. tokens.push(["#", word.substring(1)]);
  20. } else if (word[0] === '/') {
  21. // 判断{{}}之间的首字符是否为/
  22. tokens.push(["/", word.substring(1)]);
  23. } else {
  24. // 都不是
  25. tokens.push(['name', word]);
  26. }
  27. }
  28. scanner.scan(endTag);
  29. }
  30. // 返回折叠处理过的tokens
  31. return nestTokens(tokens);
  32. }

处理tokens的折叠(数据循环时需)的nestToken

  1. export default function nestTokens(tokens) {
  2. // 结果数组
  3. let nestedTokens = [];
  4. // 收集器,初始指向结果数组
  5. let collector = nestedTokens;
  6. // 栈结构,用来临时存放有循环的token
  7. let sections = [];
  8. tokens.forEach((token, index) => {
  9. switch (token[0]) {
  10. case '#':
  11. // 收集器中放token
  12. collector.push(token);
  13. // 入栈
  14. sections.push(token);
  15. // 将收集器指向当前token的第2项,且重置为空
  16. collector = token[2] = [];
  17. break;
  18. case '/':
  19. // 出栈
  20. sections.pop();
  21. // 判断栈中是否全部出完
  22. // 若栈中还有值则将收集器指向栈顶项的第2位
  23. // 否则指向结果数组
  24. collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
  25. break;
  26. default:
  27. // collector的指向是变化的
  28. // 其变化取决于sections栈的变化
  29. // 当sections要入栈的时候,collector指向其入栈项的下标2
  30. // 当sections要出栈的时候,若栈未空,指向栈顶项的下标2
  31. collector.push(token);
  32. }
  33. })
  34. return nestedTokens;
  35. }

在多层对象中深入取数据的lookup

该函数主要方便mustache取数据的。比如数据是多层的对象,模板中有{{school.class}},转换为token后是['name','school.class'],那么就能使用token[1](school.class)获取,其在data中对应的数据。然后将其替换过去。

  1. data:{
  2. school:{
  3. class:{"English Cls"}
  4. }
  5. }
  1. export default function lookup(dataObj, keyName) {
  2. // '.'.split('.') 为  ["", ""]
  3. // 若是带点的取对象属性值
  4. if (keyName.indexOf('.') !== -1 && keyName !== '.') {
  5. // 若有点符合则拆开
  6. let keys = keyName.split('.');
  7. // 存放每层对象的临时变量
  8. // 每深入一层对象,其引用就会更新为最新深入的对象
  9. // 就像是对象褪去了一层皮
  10. let temp = dataObj;
  11. keys.forEach((item) => {
  12. temp = temp[item]
  13. })
  14. return temp;
  15. }
  16. // 若没有点符号
  17. return dataObj[keyName];
  18. }

将tokens转换为Dom字符串的renderTemplate

这里有两个方法。renderTemplateparseArray在遇到#时(有数据循环时),会相互调用形成递归。

  1. export default function renderTemplate(tokens, data) {
  2. // 结果字符串
  3. let resultStr = '';
  4. tokens.forEach(token => {
  5. if (token[0] === 'text') {
  6. // 若是text直接将值进行拼接
  7. resultStr += token[1];
  8. } else if (token[0] === 'name') {
  9. // 若是name则增加name对应的data
  10. resultStr += lookup(data, token[1]);
  11. } else if (token[0] === '#') {
  12. // 递归处理循环
  13. resultStr += parseArray(token, data);
  14. }
  15. });
  16. return resultStr;
  17. }
  18. // 用以处理循环中需要的使用的token
  19. // 这里的token单独的一段token而不是整个tokens
  20. function parseArray(token, data) {
  21. // tData是当前token对应的data对象,不是整个的
  22. // 相当于data也是会在这里拆成更小的data块
  23. let tData = lookup(data, token[1]);
  24. let resultStr = '';
  25. // 在处理简单数组是的标记是{{.}}
  26. // 判断是name后lookup函数返回的是dataObj['.']
  27. // 所以直接在其递归的data中添加{'.':element}就能循环简单数组
  28. tData.forEach(element => {
  29. resultStr += renderTemplate(token[2], { ...element, '.': element });
  30. })
  31. return resultStr;
  32. }

gitee: https://gitee.com/mashiro-cat/notes-on-vue-source-code

原文链接:http://www.cnblogs.com/flytree/p/14800133.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号