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

Vue源码之mustache模板引擎(二) 手写实现mustache_第1张图片


编写示例代码

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>Documenttitle>
head>

<body>
  <h2>testh2>
  <script src="/virtual/bundle.js">script>
body>

html>

npm run dev,到http://localhost:8080/查看

Vue源码之mustache模板引擎(二) 手写实现mustache_第2张图片


实现Scanner类

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

有两个主要方法scanscanUtil

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

Vue源码之mustache模板引擎(二) 手写实现mustache_第3张图片

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>Documenttitle>
head>

<body>
  <h2>我是{{name}}, 年龄为{{age}}岁h2>
  <script src="/virtual/bundle.js">script>
  <script>
    const templateStr = `
      

我是{{name}}, 年龄为{{age}}岁

`
const data = { name: 'clz', age: 21 } const domStr = TemplateEngine.render(templateStr, data)
script> body> html>

Vue源码之mustache模板引擎(二) 手写实现mustache_第4张图片


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

Vue源码之mustache模板引擎(二) 手写实现mustache_第5张图片


提取特殊符号

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

const templateStr = `
  
    {{#arr}}
  • {{name}}喜欢的颜色是:
      {{#colors}}
    1. {{.}}
    2. {{/colors}}
  • {{/arr}}
`

Vue源码之mustache模板引擎(二) 手写实现mustache_第6张图片

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


取到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部分存好
  }
}

Vue源码之mustache模板引擎(二) 手写实现mustache_第7张图片

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

Vue源码之mustache模板引擎(二) 手写实现mustache_第8张图片


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

Vue源码之mustache模板引擎(二) 手写实现mustache_第9张图片

快成功了,开心


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

const templateStr = `
  

我是{{name}}, 年龄为{{age}}岁, 工资为{{job.salary}}元

`
const data = { name: 'clz', age: 21, job: { type: 'programmer', salary: 1 } }

Vue源码之mustache模板引擎(二) 手写实现mustache_第10张图片


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

我们再看一下上面的代码

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

把出问题的部分代进去,

result += data['job.salary']

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

Vue源码之mustache模板引擎(二) 手写实现mustache_第11张图片

那么该怎么办呢?

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

Vue源码之mustache模板引擎(二) 手写实现mustache_第12张图片


再优化一下,如果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 = `
      
    {{#arr}}
  • {{name}}喜欢的颜色是:
      {{#colors}}
    1. {{name}}
    2. {{/colors}}
  • {{/arr}}
`
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
}

Vue源码之mustache模板引擎(二) 手写实现mustache_第13张图片


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

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

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

但是,还是有问题

Vue源码之mustache模板引擎(二) 手写实现mustache_第14张图片


回到lookup中查看

Vue源码之mustache模板引擎(二) 手写实现mustache_第15张图片

微操一手:

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
}

Vue源码之mustache模板引擎(二) 手写实现mustache_第16张图片

成功。


最后把它挂到DOM树上

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

Vue源码之mustache模板引擎(二) 手写实现mustache_第17张图片


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

你可能感兴趣的:(Vue源码,Vue,Vue源码)