Vue源码之mustache模板引擎(二) 手写实现mustache

mustache.js

个人练习结果仓库(持续更新):Vue源码解析

webpack配置

可以参考之前的笔记Webpack笔记

安装:npm i -D webpack webpack-cli webpack-dev-server

webpack.config.js

const path = require('path');module.exports = {entry: path.join(__dirname, 'src', 'index.js'),mode: 'development',output: {filename: 'bundle.js',// 虚拟打包路径,bundle.js文件没有真正的生成publicPath: "/virtual/"},devServer: {// 静态文件根目录static: path.join(__dirname, 'www'),// 不压缩compress: false,port: 8080,}
}

修改package.json,更方便地使用指令


编写示例代码

src \ index.js

import { mytest } from './test.js'mytest()

src \ test.js

export const mytest = () => {console.log('1+1=2')
}

www \ index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><h2>test</h2><script src="/virtual/bundle.js"></script>
</body></html>

npm run dev,到http://localhost:8080/查看

实现Scanner类

Scanner类功能:将模板字符串根据指定字符串(如{{}})切成多部分

有两个主要方法scanscanUtil

  • scan: 跳过指定内容,无返回值
  • scanUtil:让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符

scanUtil方法

先来一下构造函数

constructor(templateStr) {this.templateStr = templateStr// 指针this.pos = 0// 尾巴,用于获取除指定符号外的内容(即`{{`和`}}`)this.tail = this.templateStr
}
// 让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
scanUtil(stopTag) {const start = this.pos  // 存放开始位置,用于返回结束前遍历过的字符// 没到指定内容时,都一直循环,尾巴也跟着变化while (this.tail.indexOf(stopTag) !== 0 && this.pos < this.templateStr.length) {    // 后面的另一个条件必须,因为最后需要跳出循环this.pos++this.tail = this.templateStr.substring(this.pos)}return this.templateStr.substring(start, this.pos)      // 返回结束前遍历过的字符
}

scan方法

// 跳过指定内容,无返回值
scan(tag) {if (this.tail.indexOf(tag) === 0) {this.pos += tag.lengththis.tail = this.templateStr.substring(this.pos)// console.log(this.tail)}
}

eos方法

因为模板字符串中需要反复使用scanscanUtil方法去把模板字符串完全切成多部份,所以需要循环,而循环结束的条件就是已经遍历完模板字符串了

// end of string:判断模板字符串是否已经走到尽头了
eos() {return this.pos === this.templateStr.length
}

完整类

/*
* 扫描器类
*/export default class Scanner {constructor(templateStr) {this.templateStr = templateStr// 指针this.pos = 0// 尾巴,用于获取除指定符号外的内容(即`{{`和`}}`)this.tail = this.templateStr}// 跳过指定内容,无返回值scan(tag) {if (this.tail.indexOf(tag) === 0) {this.pos += tag.lengththis.tail = this.templateStr.substring(this.pos)// console.log(this.tail)}}// 让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符scanUtil(stopTag) {const start = this.pos  // 存放开始位置,用于返回结束前遍历过的字符// 没到指定内容时,都一直循环,尾巴也跟着变化while (this.tail.indexOf(stopTag) !== 0 && this.pos < this.templateStr.length) {    // 后面的另一个条件必须,因为最后需要跳出循环this.pos++this.tail = this.templateStr.substring(this.pos)}return this.templateStr.substring(start, this.pos)      // 返回结束前遍历过的字符}// end of string:判断模板字符串是否已经走到尽头了eos() {return this.pos === this.templateStr.length}
}

测试使用

src / index.js

import Scanner from './Scanner.js'window.TemplateEngine = {render(templateStr, data) {// 实例化一个扫描器const scanner = new Scanner(templateStr)while (!scanner.eos()) {let words = scanner.scanUtil('{{')console.log(words)scanner.scan('{{')words = scanner.scanUtil('}}')console.log(words)scanner.scan('}}')}}
}

www / index.html

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><h2>我是{{name}}, 年龄为{{age}}岁</h2><script src="/virtual/bundle.js"></script><script>const templateStr = `<h2>我是{{name}}, 年龄为{{age}}岁</h2>`const data = {name: 'clz',age: 21}const domStr = TemplateEngine.render(templateStr, data)</script>
</body></html>

封装并实现将模板字符串编译成tokens数组

首先,把src / index.js的代码修改一下,封装成parseTemplateToTokens方法

src \ index.js

import parseTemplateToTokens from './parseTemplateToTokens.js'window.TemplateEngine = {render(templateStr, data) {const tokens = parseTemplateToTokens(templateStr)console.log(tokens)}
}

实现简单版本

// 把模板字符串编译成tokens数组
import Scanner from './Scanner.js'export default function parseTemplateToTokens() {const tokens = []// 实例化一个扫描器const scanner = new Scanner(templateStr)while (!scanner.eos()) {let words = scanner.scanUtil('{{')if (words !== '') {tokens.push(['text', words])  // 把text部分存好::左括号之前的是text}scanner.scan('{{')words = scanner.scanUtil('}}')if (words !== '') {tokens.push(['name', words])    // 把name部分存好::右括号之前的是name}scanner.scan('}}')}return tokens
}

提取特殊符号

用上一个版本的试一下,嵌套数组

const templateStr = `<ul>{{#arr}}<li>{{name}}喜欢的颜色是:<ol>{{#colors}}<li>{{.}}</li>{{/colors}}</ol></li>{{/arr}}</ul>
`

发现存在点问题,所以需要提取特殊符号#/

取到words时,判断一下第一位符号是不是特殊字符,对特殊字符进行提取

if (words !== '') {switch (words[0]) {case '#':tokens.push(['#', words.substring(1)])breakcase '/':tokens.push(['/', words.substring(1)])breakdefault:tokens.push(['text', words])// 把text部分存好}
}

又发现,还是没有实现,框框部分应该是tokens里的嵌套tokens才对

实现嵌套tokens

关键:定义一个收集器collector,一开始指向要返回的nestTokens数组,每当遇到#,则把它指向新的位置,遇到/,时,又回到上一阶,且数组是引用变量,所以给colleator push数据时,对应指向的位置也会跟着增加数据。

为了实现收集器colleator能顺利回到上一阶,那么就需要增加一个栈sections,每当遇到#时,token入栈;而当遇到/时,出栈,并判断sections是否为空,为空的话,则重新指向nestTokens,不空的话,则指向栈顶下标为2的元素。


src \ nestTokens.js

// 把#和/之间的tokens整合起来,作为#所在数组的下标为2的项export default function nestTokens(tokens) {const nestTokens = []const sections = []   // 栈结构let collector = nestTokensfor (let i = 0; i < tokens.length; i++) {const token = tokens[i]switch (token[0]) {case '#':collector.push(token)console.log(token)sections.push(token)    // 入栈token[2] = []collector = token[2]breakcase '/':sections.pop()collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokensbreakdefault:collector.push(token)}}return nestTokens
}

另外,parseTemplateToTokens函数中返回的不再是tokens,而是nestTokens(tokens)

将tokens数组结合数据解析成dom字符串

实现简单版本

直接遍历tokens数组,如果遍历的元素的第一个标记是text,则直接与要返回的字符串相加,如果是name,则需要数据data中把对应属性加入到要返回的字符串中。

src \ renderTemplate.js

export default function renderTemplate(tokens, data) {let result = ''for (let i = 0; i < tokens.length; i++) {const token = tokens[i]if (token[0] === 'text') {result += token[1]} else if (token[0] === 'name') {result += data[token[1]]}}return result
}

src \ index.js

import parseTemplateToTokens from './parseTemplateToTokens.js'
import renderTemplate from './renderTemplate.js'window.TemplateEngine = {render(templateStr, data) {const tokens = parseTemplateToTokens(templateStr)const domStr = renderTemplate(tokens, data)console.log(domStr)}
}

快成功了,开心


问题:当数据中有对象类型的数据时,会出问题。

const templateStr = `<h2>我是{{name}}, 年龄为{{age}}岁, 工资为{{job.salary}}元</h2>
`
const data = {name: 'clz',age: 21,job: {type: 'programmer',salary: 1}
}

为什么会出现这个问题呢?

我们再看一下上面的代码

if (token[0] === 'text') {result += token[1]
} else if (token[0] === 'name') {result += data[token[1]]
}

把出问题的部分代进去,

result += data['job.salary']

但是这样是不行的,JavaScript不支持对象使用数组形式时,下标为x.y的形式

那么该怎么办呢?

其实只需要把obj[x.y]的形式变为obj[x][y]的形式即可

src \ lookup.js

// 把` obj[x.y]`的形式变为`obj[x][y] `的形式export default function lookup(dataObj, keysStr) {const keys = keysStr.split('.')let temp = dataObjfor (let i = 0; i < keys.length; i++) {temp = temp[keys[i]]}return temp
}

再优化一下,如果keysStr没有.的话,那么可以直接返回

// 把` obj[x.y]`的形式变为`obj[x][y] `的形式export default function lookup(dataObj, keysStr) {if (keysStr.indexOf('.') === -1) {return dataObj[keysStr]}const keys = keysStr.split('.')let temp = dataObjfor (let i = 0; i < keys.length; i++) {temp = temp[keys[i]]}return temp
}

通过递归实现嵌套数组版本

数据以及模板字符串

const templateStr = `<ul>{{#arr}}<li>{{name}}喜欢的颜色是:<ol>{{#colors}}<li>{{name}}</li>{{/colors}}</ol></li>{{/arr}}</ul>`const data = {arr: [{name: 'clz',colors: [{name: 'red',}, {name: 'blue'}, {name: 'purple'}]},{name: 'cc',colors: [{name: 'red',}, {name: 'blue'}, {name: 'purple'}]}]}

src \ renderTemplate(增加实现嵌套数组版本)

// 将tokens数组结合数据解析成dom字符串import lookup from './lookup.js'export default function renderTemplate(tokens, data) {let result = ''for (let i = 0; i < tokens.length; i++) {const token = tokens[i]if (token[0] === 'text') {result += token[1]} else if (token[0] === 'name') {result += lookup(data, token[1])} else if (token[0] === '#') {let datas = data[token[1]]  // 拿到所有的数据数组for (let i = 0; i < datas.length; i++) {   // 遍历数据数组,实现循环result += renderTemplate(token[2], datas[i])    // 递归调用}}}return result
}

实现简单数组的那个.,因为数据中没有属性.,所以需要把该属性给加上

下面的代码只拿了改的一小段

src \ renderTemplate(增加实现嵌套数组版本)

 else if (token[0] === '#') {let datas = data[token[1]]  // 拿到所有的数据数组for (let i = 0; i < datas.length; i++) {   // 遍历数据数组,实现循环result += renderTemplate(token[2], {// 递归调用...datas[i],     // 使用扩展字符串...,把对象展开,再添加.属性为对象本身'.': datas[i]})}
}

但是,还是有问题

回到lookup中查看

微操一手:

src \ lookup.js

// 把` obj[x.y]`的形式变为`obj[x][y] `的形式export default function lookup(dataObj, keysStr) {if (keysStr.indexOf('.') === -1 || keysStr === '.') {return dataObj[keysStr]}const keys = keysStr.split('.')let temp = dataObjfor (let i = 0; i < keys.length; i++) {temp = temp[keys[i]]}return temp
}

成功。

最后把它挂到DOM树上

const domStr = TemplateEngine.render(templateStr, data)
document.getElementsByClassName('container')[0].innerHTML = domStr

学习视频:【尚硅谷】Vue源码解析之mustache模板引擎_哔哩哔哩_bilibili

Vue源码之mustache模板引擎(二) 手写实现mustache相关推荐

  1. vue源码分析-响应式系统(二)

    为了深入介绍响应式系统的内部实现原理,我们花了一整节的篇幅介绍了数据(包括data, computed,props)如何初始化成为响应式对象的过程.有了响应式数据对象的知识,上一节的后半部分我们还在保 ...

  2. vue源码解析之选项合并(二)

    选项 data 的合并策略 我们跳过mergeData 以及 mergeDataOrFn,我们暂且不关注这两个函数的作用.暂且跳过继续看下面的代码: strats.data = function (p ...

  3. 深读源码-java线程系列之自己手写一个线程池

    问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写的线程池如何测试? 简介 线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文将手把手带你写一个可用的线 ...

  4. Vue源码解读(六):update和patch

    Vue 的 _update 是实例上的一个私有方法,主要的作用就是把 VNode 渲染成真实的 DOM ,它在首次渲染和数据更新的时候被调用.在数据更新的时候会发生新 VNode 和 旧 VNode ...

  5. Vue源码解读(五):render和VNode

    Vue 2.0 相比 Vue 1.0 最大的升级就是利用了虚拟DOM. 在 Vue 1.0 中视图的更新是纯响应式的.在进行响应式初始化的时候,一个响应式数据 key 会创建一个对应的 dep,这个 ...

  6. Vue源码解析(尚硅谷)

    视频地址:Vue源码解析系列课程 一.Vue源码解析之mustache模板引擎 1. 什么是模板引擎 模板引擎是将数据要变为视图最优雅的解决方案 历史上曾经出现的数据变为视图的方法 2. mustac ...

  7. 大前端-Vue源码分析

    Vue源码解析-响应式原理 以下内容来自 拉勾教育大前端训练营 笔者在学习过程中对笔记进行的一个整理 心得体会 嘿嘿嘿~~~ 首先说说拉勾教育大前端训练营的课程视频吧,课程的质量是真的很好哦,并且已经 ...

  8. Vue响应式原理 vue源码(十一)

    前言 看过很多讲响应式的文章,大多都是告诉你们,有Observer,Dep,Wathcer类,Object.definePorperty,先会触发get中的dep.depend收集依赖,然后数据改变时 ...

  9. Vue源码之mustache模板引擎(一)

    Vue源码之mustache模板引擎(一) 个人练习结果仓库(持续更新):Vue源码解析 抽空把之前学的东西写成笔记. 学习视频链接:[尚硅谷]Vue源码解析之mustache模板引擎 模板引擎是什么 ...

最新文章

  1. Go 分布式学习利器(8)-- Go的函数
  2. 开发你的第一个 Android 应用
  3. 百度UEditor控件中的map组件不支持https使用的问题解决
  4. eclipse新建maven报错
  5. 多版本Python共存的配置和使用
  6. hadoop安装教程
  7. 于的繁体字有几种写法_“二”的繁体字有几种写法
  8. 电脑软件故障排除2014年2月16日[不断改进修正版]
  9. html 倒三角制作,css倒三角制作,css倒三角的原理
  10. html5 图形 标签,HTML5 canvas 标签介绍:定义图形
  11. 新年新气象,新的一年新的开始,给自己定个小小的目标,以此为证
  12. Hive虚拟内存溢出报错:2.9GB of 2.1GB virtual memory used. Killing container.解决办法
  13. webgl天空盒边界缝隙_基于webGL技术的3D库ThingJS支持天空盒技术实现
  14. vip html代码,vip.html
  15. android 接口实现方法,Android应用开发之Android 请求网络接口实现方法
  16. 3.4 学编程不拘于语言,学语言不限于平台——《逆袭大学》连载
  17. outlook没有显示已连接到服务器上,Outlook无法连接到服务器怎么办?
  18. 大数据运维HBase
  19. 读书忘却时间——灵魂的沉淀
  20. 回顾“低代码”历史发展,是技术进步了还是倒退了?

热门文章

  1. js手机号码验证以及隐藏中间四位数字
  2. 2015上机二 国际象棋跳马5*5
  3. Android DropBoxManagerService解析
  4. oracle左补全函数,Oracle 左侧补齐函数lpad 函数 (不积跬步,无以至千里)
  5. 现代化薪酬管理体系阶段
  6. BOSCH汽车工程手册————自适应巡航速度控制ACC
  7. 运放电路中各种电阻的计算-运算放大器
  8. 怎样查询分析邮政物流中有提前签收的快递单号
  9. 基于单片机的CO(一氧化碳)监测系统设计(#0426)
  10. 小米 信号测试软件,小米MIX 3信号实测:完胜iPhone XS Max