第四十周总结——mustache的简单实现

博客详细介绍了如何实现一个简单的Mustache模板引擎,包括模板字符串编译成tokens,tokens转换成DOM字符串的过程。涉及的关键文件有parseTemplateToTokens.js、renderTemplate.js和Scanner.js,其中nestTokens.js用于处理#和/之间的tokens。整个流程展示了如何将数据动态插入到模板中,形成最终的HTML字符串。
摘要由CSDN通过智能技术生成

实现简单的mustache

**
模板字符串(编译)=> tokens (转换)=> DOM字符串
**

模板字符串编译成tokens

index.html

<script>
        //定义templateStr字符串
        var templateStr = `
            <ul>
                {{#arr}}
                    <li>
                        i am {{name}}
                        i age is {{age}}
                        i 的爱好是
                    </li>
                {{/arr}}
            <ul/>
        `
        //定义data数据
        var data = {
            arr: [
                { name: '小1', age: 11, 'hobbies': ['音乐1', '音乐2', '音乐3', '音乐4'] },
                { name: '小2', age: 12, 'hobbies': ['音乐1', '音乐2', '音乐3', '音乐4'] },
                { name: '小3', age: 13, 'hobbies': ['音乐1', '音乐2', '音乐3', '音乐4'] }
            ]
        }
        //采用Template中的render函数让data中的数据添加到templateStr中,并返回DOM字符串
        var str=Template.render(templateStr, data)
        console.log(str);//str为返回的DOM字符串
    </script>

index.js

    index.js文件就是两个步骤,第一步调用parseTemplateToTokens函数将模板字符串转换成tokens数组,第二步调用renderTemplate将tokens数组转换成DOM字符串返回。主要还是parseTemplateToTokens.js和renderTemplate.js文件

    import parseTemplateToTokens from './parseTemplateToTokens.js'
    import renderTemplate from './renderTemplate'
    window.Template = {
        render(templateStr, data) {//templateStr为模板字符串,data为模板字符串对应的数据。
            //parseTemplateToTokens函数,让模板字符串能够变为tokens数组
            var tokens = parseTemplateToTokens(templateStr)
            //调用renderTemplateToTokens函数,让token数组变成为DOM字符串
            return renderTemplate(tokens, data)//将DOM字符串返回
        }
    }

parseTemplateToTokens.js

    该文件的作用就是将板字符串变为tokens数组,该文件引入了Scanner.js文件和nestTokens.js文件。可以先看Scanner.js文件的作用,再来看parseTemplateToTokens.js文件。nestTokens.js文件是将命名为#和/中间的数组全添加到#数组的下标为2的数组中,nestTokens.js文件是关键也是难点。

import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js'
export default function parseTemplateToTokens(templateStr) {
    var tokens = []
    //创建扫描器
    var scanner = new Scanner(templateStr)
    var words
    //扫描器工作
    while (!scanner.eos()) {
        //收集开始标记出现之前的文字
        words = scanner.scanUtil('{{')
        //存到tokens数组中,这里说明words不是在"{{"和"}}"中间的值,将words字符串命名为text
        tokens.push(['text', words])
        //跳过双大括号
        scanner.scan('{{')
        //收集开始标记出现之前的文字
        words = scanner.scanUtil('}}')
        if (words != '') {//这里说明words是在"{{"和"}}"中间的值,如果开头是#可能是循环也可能是判断,我们这里只考虑循环的情况,如果是/说明循环结束。如果不是#和/说明words是data中的一个属性值。
            //这个words就是{{}}中间的东西,判断一下首字符串
            if (words[0] == '#') {
                tokens.push(['#', words.substring(1)])
            } else if (words[0] == '/') {
                tokens.push(['/', words.substring(1)])
            } else {
                //存到tokens数组中
                tokens.push(['name', words])
            }
        }
        //跳过双大括号
        scanner.scan('}}')
    }
    //返回折叠的tokens
    return nestTokens(tokens)
}

Scanner.js

    Scanner.js是一个扫描器

文件的作用:一个{{name}}的模板字符串,当遇到"{{"或"}}"字符串后就执行scan函数,跳过模板字符串,如果不是,就返回"{{"和"}}"中间的字符串。
//扫描器类
export default class{
    constructor(templateStr){
        //让this.templateStr是传过来的模板字符串
        this.templateStr=templateStr
        //指针
        this.pos=0
        //尾巴,一开始就是模板字符串的原文
        this.tail=templateStr
    }
    //功能弱,就是走过指定内容,没有返回值
    scan(tag){
        if(this.tail.indexOf(tag)==0){
            //tag有多长,比如{{长度是2,就跳过两个长度
            this.pos+=tag.length
            this.tail=this.templateStr.substring(this.pos)
        }
    }
    //让指针进行扫描,知道遇见指定内容结束,并且能否返回结束之前路过的文字
    scanUtil(stopTag){//stopTag是Scanner.js文件传过来的值,是"{{"或"}}"
        //记录一下pos(开始值)
        const pos_backup=this.pos
        //当尾巴的开头不是stopTag的时候,就说明还没扫描到stopTag
        /*
            this.tail.indexOf(stopTag)!=0判断tail的第一位不是"{{"或"}}"
            如果不是则进入while,让pos++,再重新赋值给this.tail,
            直到this.tail.indexOf(stopTag)!=0判断不成立,也就是this.tail的第一位是
            "{{"或"}}"this.templateStr.substring(pos_backup,this.pos),也就是从开始位置pos_backup到结束位置this.pos截取this.templateStr返回。
            然后返回
        */
        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)
    }
    //指针是否已经到头,返回布尔值
    eos(){
        return this.pos>=this.templateStr.length
    }
}

nestTokens.js

    其中最关键的就是collector和nestedTokens的指向问题,这里最开始nestedTokens和collector指向一个数组,当遇到#后,nestedTokens在下标为2的地方创建一个空数组,这时候collector指向空数组,sections收集之后的内容,知道遇到/后再将sections收集的内容的最后一个数组出栈,只是后改变collector的指向,指向sections的最后一个元素,如果sections的长度为0,再次指向nestedTokens。

/**
 * 函数功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项 
 */
export default function nestTokens(tokens) {
    //nestedTokens结果数组,最终返回的数组
    var nestedTokens = []
    //栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前操作的这个tokens小数组
    var sections = []
    //收集器,天生指向nestedTokens结果数组,引用类型值,所以指向的是同一个数组
    //收集器的指向会变化,当遇到#时收集器会指向这个token的小标为2的新数组
    var collector = nestedTokens
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i]
        switch (token[0]) {
            case '#':
                //收集器中放入token
                collector.push(token)
                //入栈
                sections.push(token)
                //收集器换人,给token添加下标为2的项,并且让收集器指向它
                collector = token[2] = []/*这一行代码十分关键,
                上面写了collector.push(token),改变token,
                collector刚push的值也会跟着改变
                此时token[2]还指向collector,所以改变collector,
                collector刚push的值也会跟着改变
                */
                break
            case '/':
                //出栈,pop()会返回刚刚弹出的项
                // let section_pop = sections.pop()
                sections.pop()
                //改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
                collector = sections.length > 0 ? sections[sections.length - 1] : nestedTokens
                break
            default:
                collector.push(token)
        }
    }
    return nestedTokens
}

tokens转换成DOM字符串

renderTemplate.js

    renderTemplate.js引入了lookup.js如果name是a.b的形式,也可以获取对象中的a参数的b参数的值,引入parseArray.js,是让#里面的子元素数组也转换成DOM字符串。

/**
 * 函数的功能是让tokens数组变成dom字符串
 */
import lookup from './lookup.js'
import parseArray from './parseArray.js'
export default function renderTemplate(tokens, data) {
    //结果字符串
    var resultStr = ''
    //遍历tokens
    for (let i = 0; i < tokens.length; i++) {
        let token = tokens[i]
        //判断类型
        if (token[0] == 'text') {
            resultStr += token[1]
        } else if (token[0] == 'name') {
            resultStr += lookup(data, token[1])
        } else if (token[0] == '#') {
            resultStr += parseArray(token, data)
        }
    }
    return resultStr
}

lookup.js

/**
 * 功能是可以在dataObj对象中,寻找用连续点符号的keyName属性 
 * 这个一般会有面试题
 */
export default function lookup(dataObj, keyName) {
    //看看keyname中有没有.符号,但是不能是点本身
    if (keyName.indexOf('.') != -1 && keyName != '.') {
        var keys = keyName.split('.')
        var temp = dataObj
        for (let i = 0; i < keys.length; i++) {
            temp = temp[keys[i]]
        }
        return temp
    }
    return dataObj[keyName]
}

parseArray.js

/**
 * 处理数组,结合renderTemplate实现递归
 */
import lookup from './lookup.js'
import renderTemplate from './renderTemplate.js'
export default function parseArray(token, data) {
    //得到整体数据data中这个数组要使用的部分
    var v = lookup(data, token[1])
    //结果字符串
    var resultStr = ''
    //遍历v数组,
    for (let i = 0; i < v.length; i++) {
        //这里要补充一个'.'的使用
        resultStr += renderTemplate(token[2], {
            ...v[i],
            '.': v[i]
        })
    }
    return resultStr
}
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值