Vue 【进阶】- 模板引擎

vue的源码学习流程和知识点分析

本次您将学习到的东西

前期准备

1. 简介

1.1 什么是模板引擎

模板引擎是将数据要变为视图最优雅的解决方案

1.2 v-for 实际就是一种模板引擎

1.3 历史上曾出现的数据变为视图的方法

1.3.1 纯dom法 --- jq、js

1.3.2 数组 json 法

1.3.3 es6模板语法

2. mustache库简介

2.1 mustache库基本使用

umd 的包,浏览器和Npm node 环境都可以使用

2.1.1 下载 https://www.bootcdn.cn/

代码复制到 mustache.js

2.1.2 语法

  1. Mustache.render(tem,data) 循环对象数组
  2. Mustache.render(tem,data) 不循环
    和 Vue 的模板用法一样
  3. 循环简单数组
  4. 数组可以嵌套
  5. 布尔值(类似vue v-if)
<script src="./mustache.js"></script>
  <script>
    var tmp = `
      <ul>
        {{#arr}}
          <li>
            <div class="hd">{{name}}</div>
            <div class="bd">
              <p>{{name}}</p>
              <p>{{sex}}</p>
              <p>{{age}}</p>
            </div>
          </li>
        {{/arr}}
      </ul>
    `

    var data = {
      arr: [
        {name:'123', age: 12, sex: 1},
        {name:'123', age: 12, sex: 1},
        {name:'123', age: 12, sex: 1},
      ]
    }

    var domStr = Mustache.render(tmp, data)
    console.log(domStr)
  </script>

2.2 template 的写法

原理 由于script标签只要type不等于 text/javascript 就不会解析,可以利用来写模板,就不用写在es6的模板语法里面了

<script type="text/template" id="tmp">
    <ul>
      {{#arr}}
        <li>
          <div class="hd">{{name}}</div>
          <div class="bd">
            <p>{{name}}</p>
            <p>{{sex}}</p>
            <p>{{age}}</p>
          </div>
        </li>
      {{/arr}}
    </ul>
  </script>
  <script>
   var tmp = document.getElementById('tmp').innerHTML

2.3 mustache库不能用简单的正则表达式思路实现


string.replace()第二个参数可以是个函数,这个函数的参数就是捕获的()里的$1

2.4 mustache库的机理

2.4.1 什么是 tokens?

2.4.2 编译原理

2.4.3 循环情况下

2.4.4 双循环情况下

3. 开始手写实现 mustache

了解完 tokens,现在开始自己写一个 My_TemplateEngine 对象来实现 Mustache 的功能。我们将把独立的功能拆写为独立的 js 文件,通常是一个独立的类,每个单独的功能都应能完成独立的单元测试。
实现将模板字符串编译为 tokens

3.1 实现 Scanner 类

Scanner 类的实例就是一个扫描器,用来扫描构造时作为参数提供的那个模板字符串。

  • 属性
  • pos:指针,用于记录当前扫描到字符串的位置
  • tail:尾巴,值为当前指针之后的字符串(包括指针当前指向的那个字符)
  • 方法
  • scan:无返回值,让指针跳过传入的结束标识 stopTag
  • scanUntil:传入一个指定内容 stopTag 作为让指针 pos 结束扫描的标识,并返回扫描内容
// Scanner.js
export default class Scanner {
  constructor(templateStr) {
    this.templateStr = templateStr
    // 指针
    this.pos = 0
    // 尾巴
    this.tail = templateStr
  }

  scan(stopTag) { 
    this.pos +=  stopTag.length // 指针跳过 stopTag,比如 stopTag 是 {{,则 pos 就会加2
    this.tail = this.templateStr.substring(this.pos) // substring 不传第二个参数直接截取到末尾
  }

  scanUntil(stopTag) {
    const pos_backup = this.pos // 记录本次扫描开始时的指针位置
    // 当指针还没扫到最后面,并且当尾巴开头不是 stopTag 时,继续移动指针进行扫描
    // 注意 && 的必要性,可以避免死循环的发生
    while (!this.eos() && this.tail.indexOf(stopTag) !== 0){
      this.pos++ // 移动指针
      this.tail = this.templateStr.substring(this.pos) // 更新尾巴
    }
    return this.templateStr.substring(pos_backup, this.pos) // 返回扫描过的字符串,不包括 this.pos 处
  }
  
  // 指针是否已经抵达字符串末端,返回布尔值 eos(end of string)
  eos() {
    return this.pos >= this.templateStr.length
  }
}

3.2 根据模板字符串生成 tokens

有了 Scanner 类后,就可以着手去根据传入的模板字符串生成一个 tokens 数组了。最终想要生成的 tokens 里的每一条 token 数组的第一项用 name(数据) 或 text(非数据文本) 或 #(循环开始) 或 /(循环结束) 作为标识符。
新建一个 parseTemplateToTokens.js 文件来实现

// parseTemplateToTokens.js
import Scanner from './Scanner.js'
import nestTokens from './nestTokens' // 后面会解释

// 函数 parseTemplateToTokens
export default templateStr => {
  const tokens = []
  const scanner = new Scanner(templateStr)
  let word
  while (!scanner.eos()) {
    word = scanner.scanUntil('{{')
    word && tokens.push(['text', word]) // 保证 word 有值再往 tokens 里添加
    scanner.scan('{{')
    word = scanner.scanUntil('}}')
    /** 
     *  判断从 {{ 和 }} 之间收集到的 word 的开头是不是特殊字符 # 或 /, 
     *  如果是则这个 token 的第一个元素相应的为 # 或 /, 否则为 name
     */
    word && (word[0] === '#' ? tokens.push(['#', word.substr(1)]) : 
      word[0] === '/' ? tokens.push(['/', word]) : tokens.push(['name', word]))
    scanner.scan('}}')
  }
  return nestTokens(tokens) // 返回折叠后的 tokens, 详见下文
}

3.3 在 index.js 引入 parseTemplateToTokens

// index.js
import parseTemplateToTokens from './parseTemplateToTokens.js'

window.My_TemplateEngine = {
  render(templateStr, data) {
    const tokens = parseTemplateToTokens(templateStr)
    console.log(tokens)
  }
}

这样我们就可以把传入的 templateStr 初步转成 tokens 了,比如 templateStr 为

const templateStr = `
  <ul>
      {{#arr}}
        <li>
          <div>{{name}}的基本信息</div>
          <div>
            <p>{{name}}</p>
            <p>{{age}}</p>
            <div>
              <p>爱好:</p>
              <ol>
                {{#hobbies}}
                  <li>{{.}}</li>
                {{/hobbies}}
              </ol>
            </div>
          </div>
        </li>
      {{/arr}}
  </ul>
`

那么目前经过 parseTemplateToTokens 处理将得到如下的 tokens

接下去,就是要想办法,让循环的部分,也就是 # 和 / 之间的内容,作为以 # 为数组第 1 项元素的那个 token 的第 3 项元素。也就是把红框里的内容插入到 ["#","arr"] 内,成为其第 3 项元素;同理,将蓝框里的内容插入到 ["#","hobbies"] 内成为其第 3 项元素

3.4 实现 tokens 的嵌套

定义 handle 函数来做 tokens 的嵌套功能,将传入的 tokens 处理成包含嵌套的 handle 数组返回。

function handle(tokens) {
  let arr = [], 
      stack = [], // 入栈
      collector = arr // 收集器 - 共用一个指针

  for(var i=0;i<tokens.length;i++) {
    let token = tokens[i]

    switch(token[0]) {
      case '#':
        // 
        collector.push(token)
        // 这时 arr、stack、collector 的 token 都是共用一个指针的 token
        stack.push(token)
        // 这时 collector = token[2], collector改变,arr、stack 中 token[2] 随之改变
        collector = token[2] = []
        break;
      case '/':
        // 出栈, 返回刚刚弹出的项
        stack.pop()
        // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
        collector = stack.length > 0 ? stack[stack.length - 1][2] : arr
        break;
      default:
        collector.push(token)
        break;
    } 
  }
  return arr
}

我们已经成功实现了将模板字符串编译为 tokens 的过程,剩下的工作就是将 tokens 结合数据解析成 dom 字符串。即下图中的红框部分

3.5 tokens 结合数据解析为 dom 字符串

大致思路是遍历 tokens 数组,根据每条 token 的第一项的值来做不同的处理,为 text 就直接把 token[1] 加入到最终输出的 dom 字符串,为 name 则根据 token[1] 去 data 里获取数据,结合进来。
当 data 里存在多层嵌套的数据结构,比如 data = { test: { a: { b: 10 } } },这时如果某个 token 为 ["name", "test.a.b"],即代表数据的 token 的第 2 项元素是 test.a.b 这样的有多个点符号的值,那么我么直接通过 data[test.a.b] 是无法拿到正确的值的,因为 js 不认识这种写法。我们需要提前准备一个 lookup 函数,用以正确获取数据。

定义 lookup 函数

/**
 * @description 可以在 dataObj 对象中,寻找用连续点符号的keyName属性 data.a.b data['a.b']
 */

export default function(data, key) {
  let dataObj = data, keyArr = key.split('.')

  if(key == '.') return data

  if(keyArr.length > 1) {
    while(keyArr.length > 0) {
      dataObj = dataObj[keyArr[0]]
      keyArr.shift()
    }
  } else {
    return dataObj[key]
  }
  
  return dataObj
}

3.6 定义 renderTemplate 函数

接下来就可以开始写个 renderTemplate 函数将 tokens 和 data 作为参数传入,解析为 dom 字符串了。
需要注意的是遇到循环的情况,也就是当某个 token 的第一项为 "#" 时,要再次递归调用 renderTemplate 函数。这里我们新定义了一个 loop 函数来处理。

import lookup from "./lookup"

export default function renderTemplate(tokens, data) {
  if(!tokens) return
  let resStr = ''
  
  tokens.forEach(item => {

    switch(item[0]) {
      case 'text': 
        resStr += item[1]
        break;
      case 'key':
        resStr += lookup(data, item[1])
        break;
      case '#':
        resStr += loop(data[item[1]],item[2])
      case '.':
        break;
      default:
        break;
    }
  })

  // console.log(resStr)
  return resStr
}

function loop(data, tokens) {
  let resStr = ''
  
  data.forEach(item => {
    resStr += renderTemplate(tokens, item)
  })

  return resStr
}
posted on 2022-11-20 03:56  京鸿一瞥  阅读(770)  评论(0编辑  收藏  举报