4

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

 2 years ago
source link: https://www.clzczh.top/2022/03/28/vue-mustache-2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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
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
image-20220313161924306

实现Scanner类

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

有两个主要方法scanscanUtil

  • scan: 跳过指定内容,无返回值
  • scanUtil:让指针进行扫描,遇到指定内容才结束,还会返回结束之前遍历过的字符
image-20220314000348836
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
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
image-20220314142032812

提取特殊符号

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

const templateStr = `
  <ul>
    {{#arr}}
      <li>
        {{name}}喜欢的颜色是:
        <ol>
          {{#colors}}
            <li>{{.}}</li>
          {{/colors}}
        </ol>
      </li>
    {{/arr}}
  </ul>
`
image-20220314142439828
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
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
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
image-20220316110816619

快成功了,开心


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

const templateStr = `
  <h2>我是{{name}}, 年龄为{{age}}岁, 工资为{{job.salary}}元</h2>
`
const data = {
  name: 'clz',
  age: 21,
  job: {
    type: 'programmer',
    salary: 1
  }
}
image-20220316110854642
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
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
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
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
image-20220316142004937

回到 lookup中查看

image-20220316142324569
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
image-20220316142456833

最后把它挂到DOM树上

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK