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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 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

目录
相关文章
|
1月前
|
JavaScript API 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
6天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
1月前
|
JavaScript 前端开发 开发者
Vue是如何劫持响应式对象的
Vue是如何劫持响应式对象的
29 1
|
1月前
|
JavaScript 前端开发 API
介绍一下Vue中的响应式原理
介绍一下Vue中的响应式原理
32 1
|
1月前
|
JavaScript 前端开发 开发者
vue 数据驱动视图
总之,Vue 数据驱动视图是一种先进的理念和技术,它为前端开发带来了巨大的便利和优势。通过理解和应用这一特性,开发者能够构建出更加动态、高效、用户体验良好的前端应用。在不断发展的前端领域中,数据驱动视图将继续发挥重要作用,推动着应用界面的不断创新和进化。
|
1月前
|
JavaScript 前端开发 开发者
Vue是如何进行组件化的
Vue是如何进行组件化的
|
1月前
|
存储 JavaScript 前端开发
介绍一下Vue的核心功能
介绍一下Vue的核心功能
|
JavaScript 测试技术 容器
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1065 0
|
1月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
41 1
vue学习第一章
|
1月前
|
JavaScript 前端开发 索引
vue学习第三章
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中的v-bind指令,包括基本使用、动态绑定class及style等,希望能为你的前端学习之路提供帮助。持续关注,更多精彩内容即将呈现!🎉🎉🎉
30 1