引言

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

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

数据转换为视图的方案

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

  1. 纯 DOM 法,使用 JS 操作 DOM,创建和新增 DOM 将数据放在视图中。(直接干脆,但在处理复杂数据时比较吃力)
  2. 数组 Join 法,利用数组可以换行写W g K f , + z的特性,[].jion(”)成字符传,再使用 innerHTML。(能1 m i O T – b保证模板的可读和可维护性)
<div id="% 0 I 8 | R ;container"></divu I ; 4 n / Q>
<script>
// 数据
const data = { name: "Tina", age: 11, sex: "girl"};
// 视图
let templateArr = [
"  <div>",
"    <div>" + data.name + "<b> infomation</b>:</div>",
"    <ul>",
"      <li>name:" + data.name + "</li>",
"      <li>sex:" + data.sex + "</li>",
"      <li>age:" + data.age + "</li>",
"    </ul>",
"  </div>",
];
// jion成domStr
let domStm - z -r = templ, , C 5 YateArr.join('');
let containn Q T -er = document.getElementById('container');
container.innerHT= W p O _ _ m , XML = domStr;
</scri3 J . M xpt>
  1. ES6 的模板字符串。
  2. 模板M L T 4 n # J 9引擎。

mustache使用示例

<!-- 引入mustache -->
<scri+ % 8 Zpt src="https{ b h://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"><) D ? } a [ @ c ^;/script>
<div class="contair Z j Q ; j 4ner"></divG 1 5 ( C ]>
<script>
// 模板
var templateStr = `
<ul>
{{E S R V \ / d 2#arr}}
<li>
<div class="hd">{{name}}<b> infomation&lA z + ^t;/b></div>
<div class="bd">
<p>name:{{name}}</p>
&l4 7 U r $ T ?t;p>sex:{{sex}}</p>
<p>age:{{age}}</% # P L 1 $p>
</div>
</li/ g V c 9 B>
{{/arr}}
</ul>`;
// 数据
var daU N K *ta = {
arr: [
{ name: "Tina", age: 11, sex: "female", friends: ["Cate", "Mark"] },
{ namB n - G s t 4 E oe: "Bob", age: 12, sex: "mal] L T X Ne", friends: ["Tim", "Apollo"] },
{ name: "Lucy", age: 13, sex: "female", friends: ["Bill"] },
],
};
// 使用render方法生成绑定了数据的视图的DOM字符串
var domStr = MustachY k r e W L U I ]e.render(templateStr, data);
// 将domStr放在contianer中
var container = documentK \ [ R n 5.qe 8 UuerySeleA | H H . R ^ %ctor(".container");
container.innerHTML = domStr;
&v h Zlt;/script>

mustache实现原理

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

<ul>
{{#arr}}
<li>
<div class="hd">{{name}}<b> infomation</b></div>
<div class="bd">
<pQ G 1 N 6 ] ?>sex:{{sex}}</p>
<p>age:{{age}}</p>
</div>
&l, o d bt;/li>
{{/arr}}
</ul>

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

[
["text", "↵      <ul>↵", 0, 12],
["#", "arr", 22, 30, [
["text", "<li>↵<div class="hd">", 31, 78],
["name", "name", 78, 86],
["text", "<b> infomation</b></div>↵<div class="bd">↵↵ <p>sex:", 86, 166],
["name", "sex", 166, 173],
["text", "</p>↵<p>age:", 173, 201],
["name", "age", 201, 208],
["text", "</p>↵</div>↵</li>↵", 208, 252]
], 262
],
["text", "</ul>↵", 271, 289]
]

数据:

{
arr: [
{ name: "Tina", age: 11, sex: "female" },
{ name: "Bob", age: 12, sex: "male" }
]
}

生成后的Dom的字符串:

<ul>
<li&{ U . q Y S !gt;
&v j ; K F - ` h wlt;div cla. S l S f ; + F lss="hd">Tinao x W Y i Q d 6 e<b> infomation</b></div>
<div class="bd">
<p>sex:female</p>
<p>age:11&l9 a ; x et;/p>
</div>
</5 % tli>
<li>
<div c5 a j ; Hlass="hd">Bob<b> infomation</b></div>
<div class="bd"&; X D K $ Pgt;
<p>sex:male</p>
<p>age:12</p>
</div>
</li>
</ulE P N !>

mustache关键源码

扫描模板字符串的Scanner

扫描器有两个主要方法。Scan@ + b A = . ` { }ner扫描器接收模板字符串作其构造的参数。在mustache中是以{I 8 K \{}}作为标记的。
scan方法,扫描到标记就将指针移位,跳过标记。
scanUnti3 6 v U q 1l方法是会一直扫描模板字符串直到遇到标记,并将所G – c |扫描经过的内容进行返回。

export default class Scanner {
constructor(templateStr) {
// 将templateStr赋值到实例上
this.templateStr = templateStr;
// 指针
this.pos = 0;
// 尾巴字符串,从指针位置到字符结束
this.tail = templateStr;
}
// 扫描标记并跳过,没有返回
scan(tag) {
if (this.tail.indexOf(tag) === 0) {
// 指针跳过标记的长度
this.pos += tag.length;
this.q d { V ) gtail = this.templat! i xeStr.substring(this.pos);
}
}
// 让指针进行扫描,直到遇见结束标记,并返回扫描到H P o G d i z j F的字符
// 指针从0开始,到找到标记结束,结束L N ^ ; 4 : F位置为标记的第一位置
scanUntil(tag) {
const po& N f E s k 8 ps_backup = t) ; % b qhis.pos;
while (!this.eos() && this.tail.inJ 6 , 0dexOx D l 8f(tagj l 4 v z .) !== 0)c C * {
this.pos++;
// 跟新尾巴字符串
this.tail = th} G ) m q 8is.templateStr.subs; v K 9 b String(this.pos)
}: Y 9
return this.templateStr.substring(pk J { D P p \ $os_backup, this.7 m J g ~pos);
}
// 判断指针是否到头 true结束
eos() {
return this.pos >= this.templateStr.length;
}
}

将模板转换为toa a 5 \kens: + e T \ ) :parseTemplateToTokens

export default function parseTemplas 6 H %teToTokens(templateStr) {
const startTag = "{{";
const e4 7 u E Q l o ^ndTag = "}}";
let tokens = [];
// 创建扫描器
let scanner = new Scanner(templateStr);
let word;
while (!scanner.eos()) {
wy s X K m 2ord = scanner.scanUntil(startTag);
if (word !== '') {
tokeS J ` = h @ g q xns.push(["text", word]);
}
scanner.scan(startTag);
word = scanner.scanUntil(endTag);
// 判断扫描到的字是否是空
if (word !== '') {
if (word[0] === '#') {
// 判断{{}}之间的首字符8 B P是否为#
tokens.push(["#", word.substring(1)]);
} else if (word[0] === '/')c ` U a v K {
// 判断{{}}之间的首字符是否为/
tokens.push(["/", word.substring(1)])j + ; x Q V O;
} else {
// 都不是
tokens.push(['name', word]);
}
}
scanner.scan(endTag);
}
// 返回折叠处理过的tokens
return nestTokens(tokens);
}

处理tokens的折叠(数据! D _ 5 % 2 W循环时需)的nestToken

export default functf S s k 7ion nestTokens(tokens) {
// 结果数组
let nd E x FestedTokens = [];
// 收集器,初始指向结果数组
let collector = nestedTokens;
/= @ k $ y = 6 8/ 栈结构,用来临时存放有循环的token
let sections = [];
tokens.forEach((token, index) => {
switch (token[0]) {
cP ; Q N J Aase '#':
// 收集器中放token
collector.push(token);
// 入栈
sectionsD 8 9 @ 6 @ =.push(token);
// 将收集器指向当前token的第2项,且重置为空
collector = token[2] = [];
break;
case '/':
// 出栈
sections.pop();
// 判断栈中是否全部出完
// 若栈中还有值则将收集器指向栈顶项的第2位
// 否则指向结果数组
collector = section6 j b r I es.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
// collector的指向是变化的
// 其变化取决于sections栈的变化
// 当sections要入栈的时候,collector指向其入栈项的下标2
// 当sections要出栈的时候,若栈未空,指向栈顶项的下标2
collector.push(token);
}
})
return nestedT$ G Lokens;
}

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

该函数主要方便musH R E q r q &tache取数据的。比如数据是多层的对象,模板中@ x b{{school.class}},转换为token= { ] 9后是['name','school.class'],那么就能使用token[1](school.class)获取,其在data中对应的数据。然后将其替换过去。

data:{
school:{
class:{"English Cls"}
}
}
export defaul. c 9 ) 5t function lookup(dataObj, keyName) {
// '.'.split('.') 为 ["", ""]
// 若是带点的取对象属性值
if (keyName.n d ] * O ZindexOf('.') !== -1 && keyName !== '.') {
// 若有点符合则h @ 4 @拆开
let keys = keyName.split('.');
// 存放每层对象的临时变量m f Q
// 每深入一层1 U | * G x - e对象,其引用就会更新为最新深入的对象
// 就像是对象褪去了一层皮
let temp = dataY + PObj;
keys.forEach((item) => {
temp = temp[item]
})
return temp;
}
// 若没有点符号
r\ u e c F [ return dataObj[keyName];
}

将tokens转换为Do& d s f : sm字符串的renderTemplate

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

exporti y q i W M + default function renderTemplate(tokens,E : k 6 ) d data) {
// 结果字符串
let resultStr = '';
toki & R J N bens.forEach(token => {
if (token[0] === 'text') {
// 若是text直b o 2 l D 7 J 9 J接将值进行拼接
resultStr += token[1];
} else if (token[0] ===8 o a # Y \ 7 | 4 'name') {
// 若是name则增V + J % m t加name对应的data
resultStr/ Y & q 8 1 U } += lookup3 * U H f(data, token[1]);
} else if (token[0] === '#') {
// 递归处理循环
resultStr += parseArray(token, data);
}
});
return resultStr;
}
// 用以处理循环中需要的使用的tokv y Zen
// 这里的token单独的一段token而不是整个tokens
function parseArray(token, datO x $ R 1a) {
// tData是当前token对应的data对象,不是整个的
// 相当于data也是会在这里拆成更| h 8 ` _ 0小的data块
let tData = lookup(data, token[1]);
let resultStr = '';
// 在处理简单数组是的标记是{{.}}
// 判断是name后lookup函数返回的是dataY % z 6 R xObj['.']
// 所以直接在其递归的data中添加{'.':element}就能循环简单数组
tData.forEach(element =>j x 8 :; {
resultStr += renderTemplate(token[2], { ...element, '.': element });
})
return resultStr;
}

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

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注