Vue源码—Mustache语法的基本实现

Mustache模版语法的实现

mustache.js源码解析

<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>

在这里插入图片描述

给定模版字符串和要插入的数据

    const templateString = `
      <div>
        <ol>
          {{#students}}
          <li>
            学生{{name}}的爱好是
            <ol>
              {{#hobbies}}
              <li>{{.}}</li>
              {{/hobbies}}
            </ol>
          </li>
          {{/students}}
        </ol>
      </div>
    `;
    const templateData = {
      students: [
        {
          'name': '小红', 
          'hobbies': ['游泳', '健身'],
        },
        {
          'name': '小蓝', 
          'hobbies': ['喝水', '吃饭'],
        },
        {
          'name': '小绿', 
          'hobbies': ['游戏', '动漫'],
        }
      ]
    }
1. 模版字符串=>tokens

分别定义 头指针pos、尾字符串tail、scanUtil 和 scan 两个方法,其中

  • scanUtil:每当扫描到 {{}} 时,返回前面的子串,更新 pos和tail
  • scan:每当扫描到 {{}} 时,返回 {{}} 本身,更新 pos和tail

源码

/**
 * 扫描器类
 */
export default class Scanner {
  constructor(string) {
    this.string = string;
    this.tail = string;
    this.pos = 0;
  }

  /**
   * 如果tail为空串,返回true
   */
  eos() {
    return this.tail === '';
  }

  /**
   * 扫描tail字符串,返回匹配到regular的子串
   * 如果匹配不到,则返回空串
   */
  scan(regular) {
    let match = this.tail.match(regular);

    // 匹配不到或匹配到的起始位置不是tail的第一位
    if (!match || match.index !== 0) {
      return '';
    }

    let string = match[0];

    // 更新tail和头指针位置
    this.tail = this.tail.substring(string.length);
    this.pos += string.length;

    return string;
  }


  /**
   * 扫描tail字符串,返回匹配到regular时前面的子串
   * 如果匹配不到,则返回剩余的整个tail字符串
   */
  scanUtil(regular) {
    let index = this.tail.search(regular);
    let match;

    switch(index) {
      // 匹配不到,返回所有
      case -1:
        match = this.tail;
        this.tail = '';
        break;
      // 第一位就匹配到,则前面的子串为空串
      case 0:
        match = '';
        break;
      // 正常匹配
      default:
        match = this.tail.substring(0, index);
        this.tail = this.tail.substring(index);
    }

    //更新头指针位置
    this.pos += match.length;

    return match;
  }
}

编写解析函数,测试转换

export default function praseTemplate(templateString) {
  let tokens = [];
  let word;
  let scanner = new Scanner(templateString);
  while (!scanner.eos()) {
    word = scanner.scanUtil("{{");
    tokens.push(['text', word]);
    scanner.scan("{{");

    if (!scanner.eos()) {
      word = scanner.scanUtil("}}");
      if (word[0] === '#' || word[0] === '/') {
        tokens.push([word[0], word.substring(1)])
      } else {
        tokens.push(['name', word]);
      }
      scanner.scan("}}");
    }
  }

  return tokens;
}

测试结果

在这里插入图片描述

编写格式化函数,将tokens格式化

export default function nestTokens(tokens) {
  let nestedTokens = [];  // 结果数组,即格式化后的tokens
  let sections = [];  // 栈
  let collector = nestedTokens; // 定义一个收集器指针指向结果数组

  for (let i = 0; i < tokens.length; i++) {

    let token = tokens[i];

    switch(token[0]) {
      case '#' :
        collector.push(token);  
        sections.push(token); // 遇到#号,token入栈
        collector = token[2] = []; // 改变指针指向下一层嵌套数组头部
        break;
      case '/' :
        let section = sections.pop(); // 遇到/号,出栈
        collector = sections.length > 0 
        ? sections[sections.length - 1][2] // 当前栈不为空,则拿出栈数组最后一项
        : nestedTokens; // 当前栈为空,说明已经回归到数组最外层
        break;
      default :
        collector.push(token); // 初始引用时,相当于给nestedTokens中添加token
    }
  }

  return nestedTokens;
}

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1eUKbR4w-1626162463879)(https://i.loli.net/2021/07/13/gluqD4or15NFevm.png)]

2. tokens => dom

编写一个lookup函数

/**
 * 解析一个如“a.b.c”格式的name字符串,
 * 从data对象中取出对应属性的值。
 * @param {Object} data 
 * @param {String} name 
 */
export default function lookup(data, name) {
  if (name.indexOf('.') > 0) { //排除没有点和只有一个点的情况
    let names = name.split('.');

    let temp = data;
    for (let i = 0; i < names.length; i++) {
      temp = temp[names[i]];
    }

    return temp;
  }
  
  return data[name];
}

编写一个renderTokens函数

import lookup from './lookup'

/**
 * 使用递归,将tokens转换为dom字符串形式
 * @param {Object} tokens 
 * @param {Object} data 
 */
export default function renderTokens(tokens, data) {
  let resultStr = '';

  for (let i = 0; i < tokens.length; i++) {
    const token = tokens[i];

    if (token[0] == 'text') {
      resultStr += token[1];
    } else if (token[0] == 'name') {
      resultStr += lookup(data, token[1]);
    } else if (token[0] == '#') {
      let v = lookup(data, token[1]);
      for (let j = 0; j < v.length; j++) {
        resultStr += renderTokens(token[2], {
          '.': v[j],
          ...v[j]
        });
      }
    }
  }

  console.log(resultStr);
  return resultStr;
}

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gstRsHEw-1626162463881)(https://i.loli.net/2021/07/13/9cOZroWb87GPSuq.png)]

3. dom => html
window.MustacheTemplateEngine = {
  render(templateString, templateData) {
    let tokens = parseTemplate(templateString);
    let nestedTokens = nestTokens(tokens);
    let domStr = renderTokens(nestedTokens, templateData);
    return domStr;
  }
}

创建并操作dom节点,将最终得到的dom字符串赋值给节点,渲染到面页

<body>
  <div id="container"></div>

  <script src="/dist/bundle.js"></script>

  <script>
    const domStr = MustacheTemplateEngine.render(templateString, templateData);
    let container = document.getElementById('container')
    container.innerHTML = domStr;
  </script>
</body>

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-du82UQTm-1626162463882)(https://i.loli.net/2021/07/13/zn1hZPpWkKoFS2e.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值