MustacheJS中模板引擎核心算法之token的嵌套

126 阅读3分钟

1.什么是模板解析?

举个例子:Vue中的{{param}}写法其实就是模板语法的一种,将这种简便的模板语法与其对应的数据进行结合,生成浏览器可以解析的真实DOM的过程就是模板解析。

2.模板解析原理(模板引擎所做的工作)

对于简单的模板语法,完全可以使用正则+String.replace()对其进行替换操作。 例如下面的代码:

// 模板字符串
var templateStr = '我买了{{ thing }}, 我很{{ mood }}';

// 对应的数据
let data = {
  thing: '手机',
  mood: 'happy'
}

// 使用正则搭配replace方法对{{和}}中间的字符进行替换,替换成data[param]
let result = templateStr.replace(/\{\{( *\w+ *)\}\}/g, (captureStr, $1) => {
  console.log($1);
  return data[$1.trim()]
})
console.log(result); // 我买了手机, 我很happy

该方法只适用于简单的模板编译,但是对于复杂一点的模板,正则就显得无能为力,例如嵌套for循环等。

3.使用数组去存储代码块

<ul>
    {{#arr}}
      <li>
        <div class="hd">{{name}}的基本信息</div>
        <div class="bd">
          <p>姓名:{{name}}</p>
          <p>性别:{{sex}}</p>
          <p>年龄:{{age}}</p>
          <ol>
            {{#item.hobby}}
              <li>{{.}}</li>
            {{/item.hobby}}
          </ol>
        </div>
      </li>
    {{/arr}}
 </ul>

对于上面的模板语法,我们想把它转为形如多层数组嵌套的形式。至于为什么,就要去问问MustacheJS库的作者了,我们只需站在巨人的肩膀上即可。

微信图片_20220907185335.jpg

4.编写工具类Scanner

// Scanner.js

class Scanner {
  constructor(templateStr) {
    // 指针
    this.pos = 0;
    // 尾巴
    this.tail = templateStr;
    this.templateStr = templateStr
  }

  // 路过指定内容,无返回值
  scan(tag) {
    if(this.tail.indexOf(tag) == 0) {
      this.pos += tag.length
    }
  }

  // 指针进行扫描,遇见大括号时,返回遍历的内容 
  scanUtil(stopTag) {
    let startPos = this.pos
    while(!this.eos() && this.tail.indexOf(stopTag) != 0) {
      this.pos++;
      this.tail = this.templateStr.slice(this.pos)
    }
    return this.templateStr.slice(startPos, this.pos)
  }

  // 判断指针是否到头
  eos() {
    return this.pos >= this.templateStr.length
  }
}

5.通过Scanner工具类将模板字符串转为二维数组

import Scanner from "./Scanner";

export default function parseTemplateToTokens(templateStr) {
  let scanner = new Scanner(templateStr)
  let tokens = []
  let word = '';
  while(!scanner.eos()) {
    word = scanner.scanUtil('{{')
    if(word != '') {
      tokens.push(['text', word])
    }
    scanner.scan('{{')

    word = scanner.scanUtil('}}')
    if(word != '') {
      if(word[0] == '#') {
        tokens.push(['#', word.slice(1)])
      } else if(word[0] == '/') {
        tokens.push(['/', word.slice(1)])
      } else {
        tokens.push(['name', word])
      }
    }
    scanner.scan('}}')
  }
  return tokens
}

经过第四步和第五步,token返回的格式长这样,由于以上内容与主线无关,将直接跳过不再解释

微信图片_20220907190013.jpg

6.将tokens数组进行嵌套(重点)

// 这里定义一个方法,接受一个tokens数组,返回一个多层嵌套的tokens

export default function nestTokens(tokens) {
  // 结果数组
  let nestedTokens = []

  // 栈,用于体现当前层级的深度,其实本质上用一个数字来代替
  let sections = []

  // 指针,用于指向需要插入token的层级,其实本质上完全可以用数字替代,但因为collector回归上一级的时候需要获取到上一级的对象,所以才用的数组
  let collector = nestedTokens

  for(let i = 0; i < tokens.length; i++) {
    let token = tokens[i]
    switch(token[0]) {
      case '#':
        // 1. 首先网当前层的下标为2的数组添加该token,如果是第一次,则相当于在nestedTokens中添加
        // 说明:其实无论在nestedTokens中push,还是在token[2]处push,操作是一样的,无非就是宿主不一样,这也正是collector的作用,也就是说,我们只需要关注什么时候改变collector的指向,后续push操作就无需关注。它可以根据#和/,自由地切换插入的层级
        collector.push(token)

        // 2. 接着入栈整个token(毋庸置疑)
        sections.push(token)

        // 第三步:把当前token的下标2处开发出来,置为[],方便后续进行操作,同时collector指针下移,后续所有push操作都将push入当前token[2]中
        collector = token[2] = []
        break;
      case '/': 
        // 当扫描到/时,有两个任务,
        // 1. 首先让sections出栈(毋庸置疑)
        sections.pop()

        // 2. 接着将collector指向上一层的下标为2的数组,如果没有上一层,则collector = nestedTokens
        collector = sections.length != 0 ? sections[sections.length - 1][2] : nestedTokens
        break;
      default:
        // 当不是#和/时,秩序无脑的向collector中push元素,至于collector的切换则有上面完成
        collector.push(token)
    }
  }
  return nestedTokens
}