Vue源码之mustache模板引擎(二) 手写实现mustache
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类功能:将模板字符串根据指定字符串(如{{
和}}
)切成多部分
有两个主要方法scan和scanUtil
- 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方法
因为模板字符串中需要反复使用scan和scanUtil方法去把模板字符串完全切成多部份,所以需要循环,而循环结束的条件就是已经遍历完模板字符串了
// 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相关推荐
- vue源码分析-响应式系统(二)
为了深入介绍响应式系统的内部实现原理,我们花了一整节的篇幅介绍了数据(包括data, computed,props)如何初始化成为响应式对象的过程.有了响应式数据对象的知识,上一节的后半部分我们还在保 ...
- vue源码解析之选项合并(二)
选项 data 的合并策略 我们跳过mergeData 以及 mergeDataOrFn,我们暂且不关注这两个函数的作用.暂且跳过继续看下面的代码: strats.data = function (p ...
- 深读源码-java线程系列之自己手写一个线程池
问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写的线程池如何测试? 简介 线程池是Java并发编程中经常使用到的技术,那么自己如何动手写一个线程池呢?本文将手把手带你写一个可用的线 ...
- Vue源码解读(六):update和patch
Vue 的 _update 是实例上的一个私有方法,主要的作用就是把 VNode 渲染成真实的 DOM ,它在首次渲染和数据更新的时候被调用.在数据更新的时候会发生新 VNode 和 旧 VNode ...
- Vue源码解读(五):render和VNode
Vue 2.0 相比 Vue 1.0 最大的升级就是利用了虚拟DOM. 在 Vue 1.0 中视图的更新是纯响应式的.在进行响应式初始化的时候,一个响应式数据 key 会创建一个对应的 dep,这个 ...
- Vue源码解析(尚硅谷)
视频地址:Vue源码解析系列课程 一.Vue源码解析之mustache模板引擎 1. 什么是模板引擎 模板引擎是将数据要变为视图最优雅的解决方案 历史上曾经出现的数据变为视图的方法 2. mustac ...
- 大前端-Vue源码分析
Vue源码解析-响应式原理 以下内容来自 拉勾教育大前端训练营 笔者在学习过程中对笔记进行的一个整理 心得体会 嘿嘿嘿~~~ 首先说说拉勾教育大前端训练营的课程视频吧,课程的质量是真的很好哦,并且已经 ...
- Vue响应式原理 vue源码(十一)
前言 看过很多讲响应式的文章,大多都是告诉你们,有Observer,Dep,Wathcer类,Object.definePorperty,先会触发get中的dep.depend收集依赖,然后数据改变时 ...
- Vue源码之mustache模板引擎(一)
Vue源码之mustache模板引擎(一) 个人练习结果仓库(持续更新):Vue源码解析 抽空把之前学的东西写成笔记. 学习视频链接:[尚硅谷]Vue源码解析之mustache模板引擎 模板引擎是什么 ...
最新文章
- Go 分布式学习利器(8)-- Go的函数
- 开发你的第一个 Android 应用
- 百度UEditor控件中的map组件不支持https使用的问题解决
- eclipse新建maven报错
- 多版本Python共存的配置和使用
- hadoop安装教程
- 于的繁体字有几种写法_“二”的繁体字有几种写法
- 电脑软件故障排除2014年2月16日[不断改进修正版]
- html 倒三角制作,css倒三角制作,css倒三角的原理
- html5 图形 标签,HTML5 canvas 标签介绍:定义图形
- 新年新气象,新的一年新的开始,给自己定个小小的目标,以此为证
- Hive虚拟内存溢出报错:2.9GB of 2.1GB virtual memory used. Killing container.解决办法
- webgl天空盒边界缝隙_基于webGL技术的3D库ThingJS支持天空盒技术实现
- vip html代码,vip.html
- android 接口实现方法,Android应用开发之Android 请求网络接口实现方法
- 3.4 学编程不拘于语言,学语言不限于平台——《逆袭大学》连载
- outlook没有显示已连接到服务器上,Outlook无法连接到服务器怎么办?
- 大数据运维HBase
- 读书忘却时间——灵魂的沉淀
- 回顾“低代码”历史发展,是技术进步了还是倒退了?
热门文章
- js手机号码验证以及隐藏中间四位数字
- 2015上机二 国际象棋跳马5*5
- Android DropBoxManagerService解析
- oracle左补全函数,Oracle 左侧补齐函数lpad 函数 (不积跬步,无以至千里)
- 现代化薪酬管理体系阶段
- BOSCH汽车工程手册————自适应巡航速度控制ACC
- 运放电路中各种电阻的计算-运算放大器
- 怎样查询分析邮政物流中有提前签收的快递单号
- 基于单片机的CO(一氧化碳)监测系统设计(#0426)
- 小米 信号测试软件,小米MIX 3信号实测:完胜iPhone XS Max