引言

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

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

数据转换为视图的方案

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

  1. 纯 DOM 法,使用 JS 操作 DOM,创建和新增 DOM 将数据放在视图中。(直接干脆,但在处理复杂数据时比较吃力)
  2. 数组 Join 法,利用数组可以换行写的特性,[].jion('')成字符传,再使用 innerHTML。(能保证模板的可读和可维护性)
<div id="container"></div>

<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 domStr = templateArr.join('');
let container = document.getElementById('container');
container.innerHTML = domStr;
</script>
  1. ES6 的模板字符串。
  2. 模板引擎。

mustache使用示例

<!-- 引入mustache -->
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script> <div class="container"></div> <script>
// 模板
var templateStr = `
<ul>
{{#arr}}
<li>
<div class="hd">{{name}}<b> infomation</b></div>
<div class="bd">
<p>name:{{name}}</p>
<p>sex:{{sex}}</p>
<p>age:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>`; // 数据
var data = {
arr: [
{ name: "Tina", age: 11, sex: "female", friends: ["Cate", "Mark"] },
{ name: "Bob", age: 12, sex: "male", friends: ["Tim", "Apollo"] },
{ name: "Lucy", age: 13, sex: "female", friends: ["Bill"] },
],
}; // 使用render方法生成绑定了数据的视图的DOM字符串
var domStr = Mustache.render(templateStr, data); // 将domStr放在contianer中
var container = document.querySelector(".container");
container.innerHTML = domStr;
</script>

mustache实现原理

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



模板:

<ul>
{{#arr}}
<li>
<div class="hd">{{name}}<b> infomation</b></div>
<div class="bd">
<p>sex:{{sex}}</p>
<p>age:{{age}}</p>
</div>
</li>
{{/arr}}
</ul>

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

[
["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>
<div class="hd">Tina<b> infomation</b></div>
<div class="bd">
<p>sex:female</p>
<p>age:11</p>
</div>
</li>
<li>
<div class="hd">Bob<b> infomation</b></div>
<div class="bd">
<p>sex:male</p>
<p>age:12</p>
</div>
</li>
</ul>

mustache关键源码

扫描模板字符串的Scanner

扫描器有两个主要方法。Scanner扫描器接收模板字符串作其构造的参数。在mustache中是以{{}}作为标记的。

scan方法,扫描到标记就将指针移位,跳过标记。

scanUntil方法是会一直扫描模板字符串直到遇到标记,并将所扫描经过的内容进行返回。

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.tail = this.templateStr.substring(this.pos);
}
} // 让指针进行扫描,直到遇见结束标记,并返回扫描到的字符
// 指针从0开始,到找到标记结束,结束位置为标记的第一位置
scanUntil(tag) {
const pos_backup = this.pos;
while (!this.eos() && this.tail.indexOf(tag) !== 0) {
this.pos++;
// 跟新尾巴字符串
this.tail = this.templateStr.substring(this.pos)
}
return this.templateStr.substring(pos_backup, this.pos);
} // 判断指针是否到头 true结束
eos() {
return this.pos >= this.templateStr.length;
}
}

将模板转换为tokens的parseTemplateToTokens

export default function parseTemplateToTokens(templateStr) {
const startTag = "{{";
const endTag = "}}";
let tokens = [];
// 创建扫描器
let scanner = new Scanner(templateStr);
let word;
while (!scanner.eos()) {
word = scanner.scanUntil(startTag);
if (word !== '') {
tokens.push(["text", word]);
}
scanner.scan(startTag); word = scanner.scanUntil(endTag);
// 判断扫描到的字是否是空
if (word !== '') {
if (word[0] === '#') {
// 判断{{}}之间的首字符是否为#
tokens.push(["#", word.substring(1)]);
} else if (word[0] === '/') {
// 判断{{}}之间的首字符是否为/
tokens.push(["/", word.substring(1)]);
} else {
// 都不是
tokens.push(['name', word]);
} }
scanner.scan(endTag);
} // 返回折叠处理过的tokens
return nestTokens(tokens);
}

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

export default function nestTokens(tokens) {
// 结果数组
let nestedTokens = []; // 收集器,初始指向结果数组
let collector = nestedTokens; // 栈结构,用来临时存放有循环的token
let sections = []; tokens.forEach((token, index) => {
switch (token[0]) {
case '#':
// 收集器中放token
collector.push(token);
// 入栈
sections.push(token);
// 将收集器指向当前token的第2项,且重置为空
collector = token[2] = [];
break;
case '/':
// 出栈
sections.pop();
// 判断栈中是否全部出完
// 若栈中还有值则将收集器指向栈顶项的第2位
// 否则指向结果数组
collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
break;
default:
// collector的指向是变化的
// 其变化取决于sections栈的变化
// 当sections要入栈的时候,collector指向其入栈项的下标2
// 当sections要出栈的时候,若栈未空,指向栈顶项的下标2
collector.push(token);
}
}) return nestedTokens;
}

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

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

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

将tokens转换为Dom字符串的renderTemplate

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

export default function renderTemplate(tokens, data) {
// 结果字符串
let resultStr = '';
tokens.forEach(token => {
if (token[0] === 'text') {
// 若是text直接将值进行拼接
resultStr += token[1];
} else if (token[0] === 'name') {
// 若是name则增加name对应的data
resultStr += lookup(data, token[1]);
} else if (token[0] === '#') {
// 递归处理循环
resultStr += parseArray(token, data);
}
}); return resultStr;
} // 用以处理循环中需要的使用的token
// 这里的token单独的一段token而不是整个tokens
function parseArray(token, data) {
// tData是当前token对应的data对象,不是整个的
// 相当于data也是会在这里拆成更小的data块
let tData = lookup(data, token[1]);
let resultStr = '';
// 在处理简单数组是的标记是{{.}}
// 判断是name后lookup函数返回的是dataObj['.']
// 所以直接在其递归的data中添加{'.':element}就能循环简单数组
tData.forEach(element => {
resultStr += renderTemplate(token[2], { ...element, '.': element });
}) return resultStr;
}

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

Vue源码-手写mustache源码的更多相关文章

  1. 史上最完整promise源码手写实现

    史上最完整的promise源码实现,哈哈,之所以用这个标题,是因为开始用的标题<手写promise源码>不被收录 promise自我介绍 promise : "君子一诺千金,承诺 ...

  2. Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)

    前言 在上一篇<Spring学习之——手写Spring源码(V1.0)>中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也 ...

  3. 手写Redux-Saga源码

    上一篇文章我们分析了Redux-Thunk的源码,可以看到他的代码非常简单,只是让dispatch可以处理函数类型的action,其作者也承认对于复杂场景,Redux-Thunk并不适用,还推荐了Re ...

  4. 手写koa-static源码,深入理解静态服务器原理

    这篇文章继续前面的Koa源码系列,这个系列已经有两篇文章了: 第一篇讲解了Koa的核心架构和源码:手写Koa.js源码 第二篇讲解了@koa/router的架构和源码:手写@koa/router源码 ...

  5. 手写Tomcat源码

    http://search.bilibili.com/all?keyword=%E6%89%8B%E5%86%99Tomcat%E6%BA%90%E7%A0%81 tomcat源码分析一:https: ...

  6. Spring源码 20 手写模拟源码

    参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...

  7. 手写Vuex源码

    Vuex原理解析 Vuex是基于Vue的响应式原理基础,所以无法拿出来单独使用,必须在Vue的基础之上使用. 1.Vuex使用相关解析 main.js   import store form './s ...

  8. 手写Express.js源码

    上一篇文章我们讲了怎么用Node.js原生API来写一个web服务器,虽然代码比较丑,但是基本功能还是有的.但是一般我们不会直接用原生API来写,而是借助框架来做,比如本文要讲的Express.通过上 ...

  9. 手写Koa.js源码

    用Node.js写一个web服务器,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器 ...

  10. 手写 Java HashMap 核心源码

    手写 Java HashMap 核心源码 手写 Java HashMap 核心源码 上一章手写 LinkedList 核心源码,本章我们来手写 Java HashMap 的核心源码. 我们来先了解一下 ...

随机推荐

  1. QT 智能指针 QPointer QScopedPointer QSharedPointer QWeakPointer QSharedDataPointer 隐式共享 显示共享

    QPointer QPointer 使一种受保护的指针,当其引用的对象被销毁时,它会被自动清除(但是,销毁引用对象还是必须手动delete).QPointer所指向的对象必须是QObject或其派生类 ...

  2. 基础教材系列:编译原理——B站笔记

    一.编译器是什么 源程序→预处理器→经过预处理的源程序→编译器→汇编语言程序→汇编器→可重定位的机器代码→链接器/加载器→目标机器代码. 编译器的结构: 与源语言相关:字符流→词法分析器→词法单元流→ ...

  3. Vue3项目-生成Cron表达式组件

    最近做的一个vue3项目过程中,需要用到cron表达式功能,而对于普通业务人员,他们是不懂cron表达式规则的,所以需要做一个可手动配置生成cron表达式的功能.从网上查找了一些相关资料,然后结合vu ...

  4. Python爬取腾讯疫情实时数据并存储到mysql数据库

    思路: 在腾讯疫情数据网站F12解析网站结构,使用Python爬取当日疫情数据和历史疫情数据,分别存储到details和history两个mysql表. ①此方法用于爬取每日详细疫情数据 1 impo ...

  5. 22 axios和axios拦截器

    1. axios 由于jquery有严重的地狱回调逻辑. 再加上jquery的性能逐年跟不上市场节奏. 很多前端工程师采用axios来发送ajax. 相比jquery. axios更加灵活. 且容易使 ...

  6. #拓扑排序#洛谷 5157 [USACO18DEC]The Cow Gathering P

    题目 给出一棵树和一些限制关系 \((a_i,b_i)\), 一种合法的删点序列当且仅当删除一个点之后树的大小不超过 1 或不存在孤立点, 并且 \(a_i\) 要比 \(b_i\) 先删除,问 \( ...

  7. #Kruskal重构树,Dijkstra,倍增#洛谷 4768 [NOI2018]归程

    题目传送门 分析 首先Dijkstra是必需的(关于SPFA,它死了233) 无向图,所以先求出1号节点到所有点的距离,然后肯定希望起点能驾驶到离一号点最短的汽车可到的地方 但是怎么办,考虑海拔大的边 ...

  8. #Multi-SG#HDU 5795 A Simple Nim

    题目 有\(n\)堆石子,每次可以从一堆中取出若干个或是将一堆分成三堆非空的石子, 取完最后一颗石子获胜,问先手是否必胜 分析 它的后继还包含了分成三堆非空石子的SG函数,找规律可以发现 \[SG[x ...

  9. Go 项目依赖注入wire工具最佳实践介绍与使用

    目录 一.引入 二.控制反转与依赖注入 三.为什么需要依赖注入工具 3.1 示例 3.2 依赖注入写法与非依赖注入写法 四.wire 工具介绍与安装 4.1 wire 基本介绍 4.2 安装 五.Wi ...

  10. keycloak~RequiredActionProvider的使用

    使用场景 RequiredActionProvider,它是在认证过程中,需要当前登录的用户执行个性化的动作:当用户符合条件,就被执行RequiredActionProvider对作,当Require ...