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,更方便地使用指令

image-20220313161823530


编写示例代码

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/查看

image-20220313161924306


实现Scanner类

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

有两个主要方法scanscanUtil

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

image-20220314000348836

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.length
    this.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.length
      this.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>

image-20220314002049092


封装并实现将模板字符串编译成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
}

image-20220314142032812


提取特殊符号

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

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

image-20220314142439828

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


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

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

image-20220315184648878

又发现,还是没有实现,框框部分应该是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 = nestTokens

  for (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]
        break
      case '/':
        sections.pop()
        collector = sections.length > 0 ? sections[sections.length - 1][2] : nestTokens
        break
      default:
        collector.push(token)
    }
  }

  return nestTokens
}


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

image-20220315184914505


将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)
  }
}

image-20220316110816619

快成功了,开心


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

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

image-20220316110854642


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

我们再看一下上面的代码

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

把出问题的部分代进去,

result += data['job.salary']


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

image-20220316111512509

那么该怎么办呢?

其实只需要把 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 = dataObj

  for (let i = 0; i < keys.length; i++) {
    temp = temp[keys[i]]
  }

  return temp
}

image-20220316112721171


再优化一下,如果 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 = dataObj

  for (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
}

image-20220316141222936


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

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

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]
    })
  }
}

但是,还是有问题

image-20220316142004937


回到 lookup中查看

image-20220316142324569

微操一手:

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 = dataObj

  for (let i = 0; i < keys.length; i++) {
    temp = temp[keys[i]]
  }

  return temp
}

image-20220316142456833

成功。


最后把它挂到DOM树上

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

image-20220316143056922


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

目录
相关文章
|
9天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
105 2
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
534 0
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
2月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
105 0
|
2月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
235 1
|
3月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
362 17
|
3月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
211 1
|
5月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
755 4
|
4月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
539 77
|
5月前
|
缓存 JavaScript 前端开发
Vue 基础语法介绍
Vue 基础语法介绍