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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 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 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
9天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
9天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
9天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
8天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
10天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
8天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
23天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
10天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
15天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发

相关实验场景

更多