Vue源码-手写mustache源码
引言
在Vue中使用模板语法能够非常方便的将数据绑定到视图中,使得在开发中可以更好的聚焦到业务逻辑的开发。
mustache是一个很经典且优秀的模板引擎,vue中的模板引擎也对其有参考借鉴,了解它能更好的知道vue的模板引擎实现的原理。
数据转换为视图的方案
Vue的核心之一就是数据驱动,而模板引擎就是实现数据驱动上的很重要一环。借助模板引擎能够方便的将数据转换为视图,那么常用转换的方案有哪些呢。
- 纯 DOM 法,使用 JS 操作 DOM,创建和新增 DOM 将数据放在视图中。(直接干脆,但在处理复杂数据时比较吃力)
- 数组 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>
- ES6 的模板字符串。
- 模板引擎。
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
这里有两个方法。renderTemplate和parseArray在遇到#时(有数据循环时),会相互调用形成递归。
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源码的更多相关文章
- 史上最完整promise源码手写实现
史上最完整的promise源码实现,哈哈,之所以用这个标题,是因为开始用的标题<手写promise源码>不被收录 promise自我介绍 promise : "君子一诺千金,承诺 ...
- Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)
前言 在上一篇<Spring学习之——手写Spring源码(V1.0)>中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也 ...
- 手写Redux-Saga源码
上一篇文章我们分析了Redux-Thunk的源码,可以看到他的代码非常简单,只是让dispatch可以处理函数类型的action,其作者也承认对于复杂场景,Redux-Thunk并不适用,还推荐了Re ...
- 手写koa-static源码,深入理解静态服务器原理
这篇文章继续前面的Koa源码系列,这个系列已经有两篇文章了: 第一篇讲解了Koa的核心架构和源码:手写Koa.js源码 第二篇讲解了@koa/router的架构和源码:手写@koa/router源码 ...
- 手写Tomcat源码
http://search.bilibili.com/all?keyword=%E6%89%8B%E5%86%99Tomcat%E6%BA%90%E7%A0%81 tomcat源码分析一:https: ...
- Spring源码 20 手写模拟源码
参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...
- 手写Vuex源码
Vuex原理解析 Vuex是基于Vue的响应式原理基础,所以无法拿出来单独使用,必须在Vue的基础之上使用. 1.Vuex使用相关解析 main.js import store form './s ...
- 手写Express.js源码
上一篇文章我们讲了怎么用Node.js原生API来写一个web服务器,虽然代码比较丑,但是基本功能还是有的.但是一般我们不会直接用原生API来写,而是借助框架来做,比如本文要讲的Express.通过上 ...
- 手写Koa.js源码
用Node.js写一个web服务器,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器 ...
- 手写 Java HashMap 核心源码
手写 Java HashMap 核心源码 手写 Java HashMap 核心源码 上一章手写 LinkedList 核心源码,本章我们来手写 Java HashMap 的核心源码. 我们来先了解一下 ...
随机推荐
- 记录一次WPF命令参数报错,InvalidCastException: T for DelegateCommand<T> is not an object nor Nullable.
在使用WPF的时候对int或者bool类型进行绑定出现InvalidCastException: T for DelegateCommand is not an object nor Nullable ...
- Python 汇总列数据到行
Python汇总Excel列数据到行(方法一) import pandas as pd # 读取Excel文件 df = pd.read_excel('C:\\Users\\liuchunlin2\\ ...
- layui select onchange事件处理
1.html代码如下,不要忘记 lay-filter属性 <div class="layui-input-block"> <select id=& ...
- KingbaseES V8R3集群运维案例之---主库数据库服务down后failover切换详解
案例说明: 对KingbaseES V8R3集群,主库数据库服务down后,failover切换进行分析,详解其执行切换的过程,本案例可用于对KingbaseES V8R3集群failover故障的分 ...
- IntelliJ IDEA集成本地Maven步骤
IntelliJ IDEA集成本地Maven步骤 一.前期准备 Maven已经在本地环境配置完成,步骤可以参考我的这篇文章: https://www.cnblogs.com/rainbow-1/p/1 ...
- SpringBoot配置启动页(首页)控制台打印项目访问入口url
一.SpringBootApplication 1 package com.reliable.yang; 2 3 /** 4 * @author Administrator 5 * @date 202 ...
- #Pollard-Rho,高精度#洛谷 3499 [POI2010]NAJ-Divine Divisor
题目 给定\(m\)个数\(a_i\),令\(n=\prod_{i=1}^m a_i\), 问有多少个大于1的正整数\(d\)满足\(d^{\max k}|n\) 并输出\(\max k\),\(m\ ...
- OpenHarmony创新赛丨报名倒计时,超强秘籍带你直通大奖!
OpenHarmony创新赛报名倒计时开始啦! 设于开放原子全球开源大赛下的OpenHarmony创新赛,目前正在如火如荼地进行赛事招募中!这次大赛围绕创新应用.商显行业.金融行业三大赛题,邀请来 ...
- C#中base关键字的几种用法 (base可以对派生类(子类)实例中调用基类(父类)的构造函数方法或者基类上已经被重写的虚方法)
base最大的使用就是"面向对象"开发的多态中.base可以对派生类(子类)实例中调用基类(父类)的构造函数方法或者基类上已经被重写的虚方法. 首先声明两个类 A B public ...
- 鸿蒙智联生态产品《接入智慧生活App开发指导》(官方更新版)
原文:https://mp.weixin.qq.com/s/BDC-12aiZz2EhtjYLR7QIg,点击链接查看更多技术内容. 在HarmonyOS Connect生态产品应用开发过程中,很多开 ...