Vue源码前戏之Mustache模板引擎的源码解析

模板引擎是一个帮助我们在一个有规则的字符串内渲染数据的工具。
举一个例子:

var str = `

我叫{{name}},来自于{{school}}

`
var name = '王五' var school = '一个神秘的大学'

现在有一个str字符串,里面带有花括号的子串需要被渲染成对应的数据,得到这样的字符串,渲染到页面中,就形成了模板引擎。

我叫王五,来自于一个神秘的大学

当然mustache有更复杂的语法,比如:

var templateStr = `
    
    {{#students}}
  • 学生{{name}}的爱好是:
      {{#hobbies}}
    • {{.}}
    • {{/hobbies}}
  • {{/students}}
`
var data = { students:[ {'name':'小红','hobbies':['游泳','健身']}, {'name':'小强','hobbies':['足球','篮球','睡觉']}, {'name':'小明','hobbies':['吃饭','羽毛球']}, ] }

一. 底层tokens思想

Vue源码前戏之Mustache模板引擎的源码解析_第1张图片
mustache进行模板渲染的过程大致是这样的。
现将模板字符串变为tokens数组,再将tokens数组结合对应数据形成dom字符串。
那么tokens长什么样子呢

var str = `
	
    {{#arr}}
  • {{.}}
  • {{/arr}}
`
//str对应的tokens就长这样 [ ['text','
    '], //token ['#','arr',[ //token ['text','
  • '], //token ['name','.'], //token ['text','
  • '
    ] //token ]], ['text','
'
] //token ]

最外层数组中的每一个数组元素,都是一个token。
tokens数组将字符串分割成一个个小数组。

  • 不带{{}}目标数据的普通文本以[‘text’:’’]的形式存在。
  • 带{{}}目标数据的子串以[‘name’:‘key’]的形式存在,key就是data数据中对应的属性。
  • 若{{}}中的子串第一个元素是’#’,那么就需要特殊处理进行嵌套。

二. tokens变为dom字符串

当形成规整的tokens后,我们发现text内容可以直接拼接到结果字符串中。而当遇到name时,只需找到对应的数据然后拼接到字符串中即可。至于存在‘#’的时候,是需要特殊处理的。

三. 整体处理源码

设mustache对象,里面包含一个render方法。
render里就会有将模板字符串变为tokens数组再变为dom字符串的过程。

var mustache = {
	render(templateStr,data) {
		//将templateStr变为tokens数组
		var tokens = toToken(templateStr);
		//将tokens数组变为dom字符串
		var domStr = renderTemplate(tokens,data);
		return domStr;
	}
}

四. tokens函数

/*
  将模板字符串变为tokens数组
*/
export default function toToken(templateStr) {
  var tokens = [];

  //创建扫描器
  var scanner = new Scanner(templateStr)
  var words;   //用于收集文字

  //循环扫描模板字符串
  while(!scanner.eos()) {
    //收集开始标记前的文字
    words = scanner.scanUntil('{{');
    if(words != ''){
      tokens.push(['text',words]);
    }
    //过双大括号
    scanner.scan('{{');

    //收集标记内部的文字
    words = scanner.scanUntil('}}');
    if(words != ''){
      if(words[0] == '#') {
        tokens.push(['#',words.substring(1)]);
      } else if(words[0] == '/') {
        tokens.push(['/',words.substring(1)]);
      } else {
        tokens.push(['name',words]);
      }
    }
    scanner.scan('}}');
  }
  //返回折叠后的tokens
  return nestTokens(tokens)
}

在tokens过程中, 用到了scanner扫描器来遍历templateStr,从而将templateStr切割为上述所说的几部分。

4.1 Scanner类

/*
  扫描器类
*/

export default class Scanner {
  constructor(templateStr) {
    console.log(templateStr);
    this.templateStr = templateStr;
    //指针
    this.pos = 0;
    //尾巴,一开始就是模板字符串的原文
    this.tail = templateStr;
  }
  //功能弱,就是走过指定内容,没有返回值
  scan(stopTag) {
    if(this.tail.indexOf(stopTag) == 0) {
      //stopTag有多长,就让指针后移几位,相当于新的tail起点
      this.pos += stopTag.length;
      this.tail = this.templateStr.substr(this.pos);
    }
  }

  //让指针进行扫描,直到遇到指定内容结束,并且能够返回结束之前路过的文字
  scanUntil(stopTag) {
    //记录一下执行本方法的时候pos的值
    const pos_start = this.pos;
    //当尾巴的开头不是stopTag的时候,就说明还没扫描到stopTag
    while(!this.eos() && this.tail.indexOf(stopTag) != 0) {
      this.pos++;
      this.tail = this.templateStr.substr(this.pos);
    }
    return this.templateStr.substring(pos_start,this.pos)
  }

  //判断指针是否已经到头,返回布尔值
  eos() {
    return this.pos >= this.templateStr.length
  }
}

4.2 nestTokens函数

我们看到,在token方法里最后返回的是nestTokens(tokens)。
因为在tokens方法中,只能将字符串处理成扁平的一层,对于带’#’的没有做到嵌套。所以nestTokens的作用就是将扁平的tokens数组变为带有嵌套的tokens数组。

/*
  功能是折叠tokens,将#和/之间的tokens能够整合起来,作为他的下标为2的项
*/
export default function nestTokens(tokens) {
  //结果数组
  var nestedTokens = [];
  //栈结构,存放当前操作的token小数组
  var sections = [];
  //收集器,天生指向nestedTokens结果数组,实则是#元素需要添加第三项内容的指向
  var collector = nestedTokens;

  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] = [];
        console.log(token[1] + '入栈了');
        break;
      case '/':
        //出栈
        let section_pop = sections.pop();
        //改变收集器为栈结构对尾
        collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
        break;
      default:
        collector.push(token);
    }
  }
  return nestedTokens
}

五. renderTemplate函数

在上述过程中,我们将模板字符串templateStr变为了带有嵌套的tokens数组,接下来要做的就是,将tokens数组转换为dom字符串。

/*
  函数的功能是让tokens数组变为dom字符串
*/
import lookup from './lookup'
import parseArray from './parseArray'

export default function renderTemplate(tokens,data) {
  let 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里存的是对象是不是用点符号调用,如:student.name
      resultStr += lookup(data,token[1]);
    } else if(token[0] == '#') {
      resultStr += parseArray(token,data);
    }
  }
  return resultStr;
}

5.1 lookup函数

我们可以知道,在渲染token是以name开头时,我们需要渲染得到是后面的键值,然而有时候{{}}里面的内容是带’.'的,比如{{data.a.name}},这时我们就无法直接通过data[a.name]来获取。
因此lookup函数就是为了解决这一问题而存在。

/* 
  功能是可以在data对象中,寻找用连续点符号的keyName属性
  比如,data = {
    a: {
      b: {
        c: 100
      }
    }
  }
  那么lookup(a.b.c)的结果就是100
*/

export default function lookup(data, keyName) {
  //如果有点符号
  if(keyName.indexOf('.') != -1 && keyName != '.') {
    var keys = keyName.split('.');
    var temp = data;
    for(let i = 0; i < keys.length; i++) {
      temp = temp[keys[i]];
    }
    return temp;
  }
  //如果没有点符号
  return data[keyName];
}

5.2 parseArray函数

在处理多层嵌套的token时,我们就需要特殊处理。
其核心就是,parseArray与renderTemplate相互调用。
当遇见嵌套的token,我们可以单独用renderTemplate来处理包含在里面的token,不断叠加。

/*
  处理数组,结合renderTemplate实现递归
  注意这个函数收的参数是token,而不是tokens
  token是一个而简单的['#','students',[]]
*/
import lookup from './lookup'
import renderTemplate from './renderTemplate'
export default function parseArray(token,data) {
  //得到整体数据data中这个数组要使用的部分
  var v = lookup(data,token[1]);
  var resultStr = '';
  //他要遍历数据,数据有几条,就要有几个这种dom
  for(let i = 0; i < v.length; i++) {
    resultStr += renderTemplate(token[2],{
      ...v[i],
      '.':v[i]
    });
  }
  return resultStr;
}

这就是完整的模板字符串被渲染的过程了,如有问题多指教。

你可能感兴趣的:(Vue系列,html,vue.js,前端,es6,javascript)