【Vue源码解析】mustache模板引擎

news2024/12/26 21:24:48

模板引擎

        • 什么是模板引擎
        • 实现 Scanner 类
        • 根据模板字符串生成 tokens
        • 在 index.js 引入 parseTemplateToTokens
        • 实现 tokens 的嵌套
        • One More Thing
        • tokens 结合数据解析为 dom 字符串
        • 定义 lookup 函数
        • 定义 renderTemplate 函数

mustache模板引擎 - 01

什么是模板引擎

模板引擎是将数据变为视图最优雅的解决方案

以前出现过的其它数据变视图的方法

  • 纯 DOM 法

  • 数组 join 法
    在 js 里单双引号内的内容是不能换行的,为了提高 dom 结构可读性,利用了数组的 join 方法(将数组变为字符串),注意 join 的参数 ‘’ 不可以省略,否则得到的 str 字符串会是以逗号间隔的

  • es6 的模板字符串(``)

刚开始用模板引擎时可以引用 如下:

<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>

底层核心机理

//最简单的模板的实现机理,利用的是正则表达式中的replace()方法

​ // replace()的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,就是$1

​ //结合data的对象,即可进行智能的替换

在这里插入图片描述

//编译普通对象成token
const templateStr = `<h3>我今天买了一部{{thing}}手机,花了我{{money}}元,心情好{{mood}}啊</h3>`;

[
    ["text",'<h3>我今天买了一部'],
    ["name",'thing'],
    ["text",'手机,花了我'],
    ["name",'money']
    ["text",'元,心情好'],
    ["name","mood"],
    ["text",'啊']
]

模块化打包工具有webpack(webpack-dev-server) 、rollup、Parcel等
我们经常使用webpack进行模块化打包

实现 Scanner 类

Scanner 类的实例就是一个扫描器,用来扫描构造时作为参数提供的那个模板字符串

- 属性

  • pos:指针,用于记录当前扫描到字符串的位置
  • tail:尾巴,值为当前指针之后的字符串(包括指针当前指向的那个字符)

- 方法

  • scan:无返回值,让指针跳过传入的结束标识 stopTag
  • scanUntil:传入一个指定内容 stopTag 作为让指针 pos 结束扫描的标识,并返回扫描内容
// Scanner.js
export default class Scanner {
  constructor(templateStr) {
    this.templateStr = templateStr
    // 指针
    this.pos = 0
    // 尾巴
    this.tail = templateStr
  }
  scan(stopTag) { 
    this.pos +=  stopTag.length // 指针跳过 stopTag,比如 stopTag 是 {{,则 pos 就会加2
    this.tail = this.templateStr.substring(this.pos) // substring 不传第二个参数直接截取到末尾
  }

  scanUntil(stopTag) {
    const pos_backup = this.pos // 记录本次扫描开始时的指针位置
    // 当指针还没扫到最后面,并且当尾巴开头不是 stopTag 时,继续移动指针进行扫描
    while (!this.eos() && this.tail.indexOf(stopTag) !== 0){
      this.pos++ // 移动指针
      this.tail = this.templateStr.substring(this.pos) // 更新尾巴
    }
    return this.templateStr.substring(pos_backup, this.pos) // 返回扫描过的字符串,不包括 this.pos 处
  }
  
  // 指针是否已经抵达字符串末端,返回布尔值 eos(end of string)
  eos() {
    return this.pos >= this.templateStr.length
  }
}

根据模板字符串生成 tokens

tokens是一个JS的嵌套数组,说白了,就是模板字符串的JS表示

有了 Scanner 类后,就可以着手去根据传入的模板字符串生成一个 tokens 数组了。最终想要生成的 tokens 里的每一条 token 数组的第一项用 name(数据) 或 text(非数据文本) 或 #(循环开始) 或 /(循环结束) 作为标识符

// parseTemplateToTokens.js
import Scanner from './Scanner.js'
import nestTokens from './nestTokens' // 后面会解释

// 函数 parseTemplateToTokens
export default templateStr => {
  const tokens = []
  const scanner = new Scanner(templateStr)
  let word
  while (!scanner.eos()) {
    word = scanner.scanUntil('{{')
    word && tokens.push(['text', word]) // 保证 word 有值再往 tokens 里添加
    scanner.scan('{{')
    word = scanner.scanUntil('}}')
    /** 
     *  判断从 {{ 和 }} 之间收集到的 word 的开头是不是特殊字符 # 或 /, 
     *  如果是则这个 token 的第一个元素相应的为 # 或 /, 否则为 name
     */
    word && (word[0] === '#' ? tokens.push(['#', word.substr(1)]) : 
      word[0] === '/' ? tokens.push(['/', word]) : tokens.push(['name', word]))
    scanner.scan('}}')
  }
  return nestTokens(tokens) // 返回折叠后的 tokens, 详见下文
}

在 index.js 引入 parseTemplateToTokens

// index.js
import parseTemplateToTokens from './parseTemplateToTokens.js'

window.My_TemplateEngine = {
  render(templateStr, data) {
    const tokens = parseTemplateToTokens(templateStr)
    console.log(tokens)
  }
}

这样我们就可以把传入的 templateStr 初步转成 tokens 了,比如 templateStr 为

const templateStr = `
  <ul>
      {{#arr}}
        <li>
          <div>{{name}}的基本信息</div>
          <div>
            <p>{{name}}</p>
            <p>{{age}}</p>
            <div>
              <p>爱好:</p>
              <ol>
                {{#hobbies}}
                  <li>{{.}}</li>
                {{/hobbies}}
              </ol>
            </div>
          </div>
        </li>
      {{/arr}}
  </ul>
`

那么目前经过 parseTemplateToTokens 处理将得到如下的 tokens
image (1).png

实现 tokens 的嵌套

新建 nestTokens.js 文件,定义 nestTokens 函数来做 tokens 的嵌套功能,将传入的 tokens 处理成包含嵌套的 nestTokens 数组返回。

然后在 parseTemplateToTokens.js 引入 nestTokens,在最后 return nestTokens(tokens)

在 nestTokens 中,我们遍历传入的 tokens 的每一个 token,遇到第一项是 # 和 /的分别做处理,其余的做一个默认处理。

大致思路是当遍历到的 token 的第一项为 # 时,就把直至遇到配套的 / 之前,遍历到的每一个 token 都放入一个容器(collector)中,把这个容器放入当前 token 里作为第 3 项元素

但这里有个问题:在遇到匹配的 / 之前又遇到 # 了怎么办?也就是如何解决循环里面嵌套循环的情况?

  • 解决方法就是新建一个 栈数据类型 的数组(stack),遇到一个 #,就把当前 token 放入这个栈中,让 collector 指向这个 token 的第三个元素。
    遇到下一个 # 就把新的 token 放入栈中,collector 指向新的 token 的第三个元素。
    遇到 / 就把栈顶的 token 移出栈,collector 指向移出完后的栈顶 token。
    这就利用了栈的先进后出的特点,保证了遍历的每个 token 都能放在正确的地方,也就是 collector 都能指向正确的地址。
// nestTokens.js
export default (tokens) => {
  const nestTokens = []
  const stack = []
  let collector = nestTokens // 一开始让收集器 collector 指向最终返回的数组 nestTokens
  tokens.forEach(token => {
    switch (token[0]) {
      case '#':
        stack.push(token)
        collector.push(token)
        collector = token[2] = [] // 连等赋值
        break
      case '/':
        stack.pop(token)
        collector = stack.length > 0 ? stack[stack.length-1][2] : nestTokens
        break;
      default:
        collector.push(token)
        break
    }
  })
  return nestTokens
}

One More Thing

上面的代码中有用到 collector = token[2] = [],是为连等赋值,相当于

token[2] = []
collector = token[2]

看着简单,其实暗含着小坑,除非你真的了解它,否则尽量不要使用。比如我在别处看到这么一个例子,

let a = {n:1};
a.x = a = {n:2};
console.log(a.x); // 输出? 

答案是 undefined,你做对了吗?

2021-04-29_111352.png

tokens 结合数据解析为 dom 字符串

大致思路是遍历 tokens 数组,根据每条 token 的第一项的值来做不同的处理,为 text 就直接把 token[1]
加入到最终输出的 dom 字符串,为 name 则根据 token[1] 去 data 里获取数据,结合进来。

当 data 里存在多层嵌套的数据结构,比如 data = { test: { a: { b: 10 } } },这时如果某个 token 为 [“name”, “test.a.b”],即代表数据的 token 的第 2 项元素是 test.a.b 这样的有多个点符号的值,那么我么直接通过 data[test.a.b] 是无法拿到正确的值的,因为 js 不认识这种写法。
我们需要提前准备一个 lookup 函数,用以正确获取数据。

定义 lookup 函数

// lookup.js
// 思路就是先获取 test.a 的值, 比如说是 temp, 再获取 temp.b 的值, 一步步获取
export default (data, key) => {
  // 如果传入的 key 里有点符号而且不是仅仅只是点符号
  if (key.indexOf('.') !== -1 && key !== '.' ) {
    const keys = key.split('.') // 将 key 用 . 分割成一个数组
    return keys.reduce((acc, cur) => {
      return acc[cur] // 一步步获取
    }, data)
  }
  // 如果传入的 key 没有点符号,直接返回
  return data[key]
}

定义 renderTemplate 函数

接下来写个 renderTemplate 函数将 tokens 和 data 作为参数传入,解析为 dom 字符串了。

// renderTemplate.js
import lookup from './lookup.js'
import parseArray from './parseArray.js'

export default (tokens, data) => {
  let domString = ''
  tokens.forEach(token => {  
    switch (token[0]) {
      case 'text':
        domString += token[1]
        break
      case 'name':
        domString += lookup(data, token[1])
        break
      case '#':
        domString += parseArray(token[2], data[token[1]])
        break
      default:
        break
    }
  }) 
  return domString
}
复制代码

需要注意的是遇到循环的情况,也就是当某个 token 的第一项为 “#” 时,要再次递归调用 renderTemplate 函数。这里我们新定义了一个 parseArray 函数来处理。

// parseArray.js
import renderTemplate from './renderTemplate.js'
export default (tokens, data) => {
  let domString = ''
  data.forEach(itemData => {
    domString += renderTemplate(tokens, {
      ...itemData,
      '.': itemData // 针对简单数组的情况,即模板字符串里的 {{.}} 
    })
  })
  return domString
}
复制代码

到此,就算是完结了。能力一般,水平有限,难免会有纰漏不足的地方,还请各位看官斧正。

感谢.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/77202.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何清除chrome浏览器缓存

清除浏览器的缓存知识调用前言引入具体操作知识调用 文章中可能用到的知识点前端学习&#xff1a;浏览器缓存方式有哪些&#xff08;http协议 websql indexDB cookie localstorage sessionstorage&#xff09;如何查看Chrome浏览器的页面缓存内容【详细教程】 前言引入 上期文…

微服务框架 SpringCloud微服务架构 微服务保护 31 限流规则 31.1 簇点链路

微服务框架 【SpringCloudRabbitMQDockerRedis搜索分布式&#xff0c;系统详解springcloud微服务技术栈课程|黑马程序员Java微服务】 微服务保护 文章目录微服务框架微服务保护31 限流规则31.1 簇点链路31.1.1 簇点链路31.1.2 快速入门31 限流规则 31.1 簇点链路 31.1.1 簇…

52、网络

目录 一、网络通信 二、网络 三、 ip地址 四、域名 五、端口号 六、网络通信协议 1、协议&#xff08;tcp/ip&#xff09; 2、TCP和UDP 七、InetAddress类 八、Socket 1、基本介绍&#xff1a; 2、TCP网络通信编程 3、UDP网络通信编程&#xff08;了解即可&#xff0c…

完整版JAVA物业管理系统源码带小程序+文字安装教程+视频

这套系统还包含了小程序前端源码。 技术架构 技术框架&#xff1a;springboot ssm shiro layui 运行环境&#xff1a;IntelliJ IDEA 2022 jdk1.8 Mysql5.7.4 maven nginx 宝塔面板 文字安装教程 1.下载源码后打开小皮面板&#xff0c;安装mysql5.7数据库&#xff0c;创…

【JavaEE】计算机是怎样工作的,五分钟带你理解计算机!!!

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀学瓜的主页&#x1f319; 专栏&#xff1a;JavaEE 分享&#xff1a;一、让你知道我的存在&#xff1b;二、让你存在下去&#xff0c;对我来说都是危险的&#xff0c;都违反第一条公理。 ——《三体》 主要内容&#xff1a;…

C++11标准模板(STL)- 算法(std::push_heap)

定义于头文件 <algorithm> 算法库提供大量用途的函数&#xff08;例如查找、排序、计数、操作&#xff09;&#xff0c;它们在元素范围上操作。注意范围定义为 [first, last) &#xff0c;其中 last 指代要查询或修改的最后元素的后一个元素。 数据结构的堆物理结构是数…

Linux离线安装RabbitMQ

使用docker离线安装rabbitmq 1.在外网环境下载rabbitmq:management镜像 命令&#xff1a;docker pull rabbitmq:management 启动容器&#xff08;即验证镜像是否有用&#xff09;&#xff1a; 命令&#xff1a;docker run -d --hostname wxrabbit --name rabbitmq -p 15672:15…

【深度学习】学习率预热和学习率衰减 (learning rate warmup decay)

背景 在深度学习中学习率这个超参数&#xff0c;在选取和调整都是有一定策略的&#xff0c;俗称炼丹。有时我们遇到 loss 变成 NaN 的情况大多数是由于学习率选择不当引起的。 神经网络在刚开始训练的时候模型的权重(weights)是随机初始化的&#xff0c;选择一个较大的学习率…

干货 | 浅谈携程大住宿研发效能提升实践

作者简介Mia &#xff0c;携程高级项目经理&#xff0c;负责酒店Devops实践&#xff0c;关注Devops/敏捷等领域。YY&#xff0c;携程敏捷教练&#xff0c;负责团队敏捷转型&#xff0c;研发效能提升实践&#xff0c;关注Agile、Devops、研发效能等领域。一、前言管理大师彼得德…

[ChatGPT为你支招]如何提高博客的质量,找到写作方向,保持动力,增加粉丝数?

0. 引言 作为一个博主&#xff0c;您可能会面临很多挑战&#xff0c;比如如何提高博客的质量&#xff0c;如何找到自己的写作方向&#xff0c;如何保持持续写作的动力&#xff0c;以及如何增加博客的粉丝数量。在这篇文章中&#xff0c;我们将为您提供一些有用的建议&#xff…

Maven打包报错:找不到符号,类BASE64Encoder,程序包sun.misc

背景 一个基于若依单体架构的多模块 Maven 项目的国产化迁移适配&#xff0c;由于是客户的代码&#xff0c;我们不用关心具体的功能实现&#xff0c;直接来做迁移即可。实施时&#xff0c;按照我们总结的整改建议调整源码&#xff0c;具体迁移适配过程可参考本专栏的其他文章。…

ADI Blackfin DSP处理器-BF533的开发详解19:LAN的网口设计

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 功能介绍 BF533说实话用来做LAN的应用有些许勉强&#xff0c;因为他自己不带网口&#xff0c;要做的话&#xff0c;需要在总线上挂&#xff0c;那…

彻底搞懂ESLint与Prettier在vscode中的代码自动格式化

前言 前端代码格式化社区提供了两种比较常用的工具ESLint和Prettier&#xff0c;他们分别提供了对应的vscode的插件&#xff0c;二者在代码格式化方面有重叠的部分&#xff0c;规则不一致时会导致冲突。vscode作为前端开发编辑器已经越来越普遍了&#xff0c;这需要开发者在vs…

ChatGPT与搜索引擎合体,谷歌都不香了,LeCun转发|在线可玩

Alex Pine 发自 凹非寺量子位 | 公众号 QbitAI见惯了列表式搜索引擎&#xff0c;你有没有想过给它换种画风&#xff1f;有人脑洞大开&#xff0c;把艳惊四座的ChatGPT和必应搜索结合起来&#xff0c;搞出了一个智能搜索引擎&#xff1a;既有ChatGPT式的问答&#xff0c;又像普通…

VS——路径说明

$(TargetDir)输出目标的路径 $(TargetPath) 输出文件.exe的路径 $(TargetName) 项目名字 $(TargetFileName) 输出的.exe的名字 $(TargetExt) 文件的扩展名 $(ProjectDir)工程目录 目录根据下面的文件来的 $(IntDir)获取中间文件 $(OutDir)文件输出路径 $(Solu…

神马操作Kafka 竟宣布弃用 Java 8

Kafka 3.0.0 发布了&#xff1a; ​ 编辑切换为居中 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 主要更新如下&#xff1a; The deprecation of support for Java 8 and Scala 2.12 Kafka Raft support for snapshots of the metadata topic and ot…

ADI Blackfin DSP处理器-BF533的开发详解15:RS232串口的实现(含源代码)

硬件准备 ADSP-EDU-BF533&#xff1a;BF533开发板 AD-HP530ICE&#xff1a;ADI DSP仿真器 软件准备 Visual DSP软件 硬件链接 实现原理 ADSP-EDU-BF533 开发板上设计了一个 RS232 接口&#xff0c;该接口通过 ADSP-BF53x 上的 UART 接口&#xff0c;扩展 RS232协议的芯片实…

Java学习之Object类——equals方法

Object 类是类层次结构的根类。每个类都使用 Object 作为超类。所有对象&#xff08;包括数组&#xff09;都实现这个类的方法&#xff0c;学习Object 类的六个方法——equals(Object obj)、finalize、toString、hashCode、getClass、clone 目录 和equals的对比 一、的作用 …

ChatGPT惊人语录大赏

文 | 智商掉了一地这几天ChatGPT实在太火了&#xff0c;笔者的朋友圈已经被ChatGPT的各种金句刷屏了&#xff0c;实在忍不住整理下来&#xff0c;分享给大家。ChatGPT惊人语录1&#xff1a;建议娶奶奶为妻注&#xff1a;贾母是贾宝玉的奶奶ChatGPT惊人语录2&#xff1a;角色扮演…

【allegro 17.4软件操作保姆级教程十】文件输出

目录 1.1添加光绘层叠 1.1.1添加线路层 1.1.2添加表底阻焊层 1.1.3添加表底钢网层 1.1.4添加表底丝印层 1.1.5添加钻孔层 ​1.2输出文件 1.2.1输出光绘文件 1.2.2输出钻孔文件 1.2.3输出坐标文件 1.2.4输出文件打包 1.1添加光绘层叠 在输出文件之前需要先添加光绘层…