Vue source code analysis mustache template engine study notes + handwriting to complete the mustache function (castrated version)

1. What is a template engine?

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

1. The development history of converting data into views

1.1 Pure DOM method:

Rendering:
Insert image description here

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')

        for (var i =0 ; i<arr.length;i++){
      
      
            // 没遍历一项,都要用DOM方法去创建li标签
            let oLi = document.createElement('li')
            // 创建hd这个div
            let hdDiv = document.createElement('div')
            hdDiv.className = 'hd'
            hdDiv.innerText = arr[i].name + '的基本信息'
            // 创建bd这个div
            let bdDiv = document.createElement('div')
            bdDiv.className = 'bd'
            // 创建三个p标签
            let p1 = document.createElement('p')
            p1.innerText = '姓名:' + arr[i].name

            let p2 = document.createElement('p')
            p2.innerText = '年龄:' + arr[i].age

            let p3 = document.createElement('p')
            p3.innerText = '性别:' + arr[i].sex

            //创建的节点都是孤儿节点,所以必须上树才能被用户看到
            oLi.appendChild(hdDiv)

            hdDiv.appendChild(bdDiv)

            bdDiv.appendChild(p1)

            bdDiv.appendChild(p2)

            bdDiv.appendChild(p3)
            
            list.appendChild(oLi)
        }
    </script>
</body>
</html>

Summary:
Every step of converting pure DOM operation data into views involves operating DOM. This not only has a large page overhead, but also is very troublesome. It needs to be created and climbed up the tree every time. The data and view are highly coupled and difficult to identify. Code reading Sex is also bad.

1.2 Array join method:

Early background:
Since the character content defined by the string " " ' ' does not allow line breaks, backtick marks are a new syntax of ES6 that did not exist before. However, arrays can be displayed in line breaks, which can display the level of the structure, and then join('') Method can realize character splicing display

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')
        // 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
        for (let i =0; i< arr.length;i++){
      
      
            list.innerHTML += [
                '<li>',
                '       <div class="hd">' + arr[i].name + '的信息</div>',
                '       <div class="bd">',
                '           <p>姓名:'+ arr[i].name +'</p>',
                '           <p>年龄:'+ arr[i].age +'</p>',
                '           <p>性别:'+ arr[i].sex +'</p>',
                '       </div>',
                '</li>'
            ].join('')
                    
        }
    </script>
</body>
</html>

Summary:
The array join method is cleverly used, which greatly reduces the amount of code. At the same time, because line breaks can be made, the structure is not so obscure, but the code and data are still somewhat sticky, and the view and data cannot be separated.

1.3ES6 anti-boot method:

<!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>
    <ul id="list">

    </ul>
    <script>
        var arr = [
            {
      
      "name":"某可","age":18,"sex":"男"},
            {
      
      "name":"某鸿","age":18,"sex":"男"},
            {
      
      "name":"小李","age":5,"sex":"女"}
        ]

        var list = document.getElementById('list')
        // 遍历arr数组,每遍历一项,就以字符串的视角将HTML字符串添加到list中
        for (let i =0; i< arr.length;i++){
      
      
            list.innerHTML += `
                <li>
                    <div class='hd'>${ 
        arr[i].name}的基本信息</div>
                    <div class='bd'>
                        <p>姓名:${ 
        arr[i].name}</p>
                        <p>年龄:${ 
        arr[i].age}</p>
                        <p>性别:${ 
        arr[i].sex}</p>
                    </div>
                </li>
            `
            

        }
    </script>
</body>
</html>

Summary:
Compared with arrays, it simplifies a lot of writing methods and reduces the difficulty of writing. After all, it is a new syntax, and the effect is not much different from the previous two. However, I heard from the lecturer that writing this way requires one more string parsing. The process is slower than the stupidest writing method, but it is convenient to write. I think the cost can also be reduced through the createDocumentFragment() method.

1.4 Template engine:

<script>
        var templateStr = `
            <ul>
                {
       
       {#arr}}
                    <li>
                        <div class='hd'>{
       
       {name}}的基本信息</div>
                        <div class='bd'>
                            <p>姓名:{
       
       {name}}</p>
                            <p>年龄:{
       
       {age}}</p>
                            <p>性别:{
       
       {sex}}</p>
                        </div>
                    </li>
                {
       
       {/arr}}
            </ul>
        `
        var data = {
      
      
                 arr : [
                    {
      
      "name":"某可","age":18,"sex":"男"},
                    {
      
      "name":"某鸿","age":18,"sex":"男"},
                    {
      
      "name":"小李","age":5,"sex":"女"}
                ]
        }

        var domStr = mustache.render(templateStr,data)
        var container = document.getElementById('container')
        container.innerHTML = domStr
    </script>

2.Mustache template engine implementation principle:

①Compile the template string into tokens form

② Combine tokens with data and parse them into DOM strings

Insert image description here

1.tokens:

Tokens is a JS nested array, which is the JS representation of template string.
It is the originator of "abstract syntax tree", "virtual node", etc.
Template string:

<h1> 我买了一个{
   
   {thing}},好{
   
   {mood}}啊</h1>
tokens:
其中每一项都是一个token,例如:["text",",好"],加起来就是tokens
[
	["text","<h1> 我买了一个"],
	["name","thing"],
	["text",",好"],
	["name","mood"],
	["text","啊</h1>"]
]

When there is a loop, it will be compiled into more deeply nested tokens.

<div>
	<ul>
		{
   
   {#arr}}
		<li>{
   
   {.}}</li>
		{
   
   {/arr}}
	</ul>
</div>


[
	["text","<div><ul>"],
	["#","arr",[
		["text","<li>"],
		["name","."],
		["text","</li>"]
	]],
	["text","</ul></div>]
]

2. Handwritten implementation of the mustache library

1. Create Scanner class

1.1 Function:

扫描字符串,为模板字符串转换成tokens做服务

1.2 Code:

Define Scanner class
The function is to find { {or }} to split the content.
export default class Scanner {
    
    
    constructor(templateStr){
    
    
        // 将模板字符串写到自己的属性上,或者说实例接收参数
        this.templateStr = templateStr
        // 指针
        this.pos = 0
        // 尾巴,开始就是原文
        this.tail = templateStr
    }
    // 功能简单,就是走过指定内容,没返回值
    scan(tag){
    
    
        if(this.tail.indexOf(tag) == 0){
    
    
            // tag有多长,就后移多少位
            this.pos += tag.length
            // 改变尾巴
            this.tail = this.templateStr.substring(this.pos)
        }
    }

    // 让指针进行扫描,直到遇到{
    
    {}}指定内容,并且返回结束之前的路过的文字内容
    scanUtil(stopTag){
    
    
        // 记录一下执行本方法的时候pos的值
        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)
    }

    // 指针是否已经到头,返回布尔值
    eos(){
    
    
        return this.pos >= this.templateStr.length
    }
}
Create and expose the parseTemplateToTokens method:
The function is to convert template characters into tokens array
import Scanner from './Scanner.js'

// 将模板字符串变为tokens数组

import Scanner from './Scanner.js'
import nestTokens from './nestTokens.js';

// 将模板字符串变为tokens数组

export default function parseTemplateToTokens(templateStr){
    
    
    var tokens = []
    // 创建扫描器
    var scanner = new Scanner(templateStr)
        // 让扫描器工作
        var words;
        while(!scanner.eos()){
    
    
            // 收集标记出现之前的文字
            words = scanner.scanUtil('{
    
    {')
            if(words !=''){
    
    
                // 需要去掉空格,但是不能去掉class
                var isInJJH = false
                var _words =''
                for (let i =0;i<words.length;i++){
    
    
                    // 判断是否在标签里面
                    if(words[i] =='<'){
    
    
                        isInJJH = true
                    }else if (words[i] =='>'){
    
    
                        isInJJH = false
                    }
                    // 如果这项不是空格,拼接
                    if(words[i]!=' '){
    
    
                        _words += words[i]
                    }else{
    
    
                        if(isInJJH){
    
    
                            _words+=words[i]
                        }
                    }
                }
                tokens.push(['text',_words])
            }
            scanner.scan('{
    
    {')

            // 收集{
    
    {}}之间的文字
            words = scanner.scanUtil('}}')
            if(words !=''){
    
    
                // 判断首字母是不是#
                if (words[0] == '#'){
    
    
                    //存起来,下标从1开始,表示是#
                    tokens.push(['#',words.substring(1)])
                }else if (words[0] == '/'){
    
    
                    //存起来,下标从1开始,表示是/
                    tokens.push(['/',words.substring(1)])
                }else{
    
    
                    tokens.push(['name',words])
                }
                
            }
            scanner.scan('}}')
        }
        // 返回折叠收集的tokens
        // console.log(tokens)
    return nestTokens(tokens)
}
Use test:
import parseTemplateToTokens from './parseTemplateToTokens'

window.YSZ_TemplateEngine = {
    
    
    render(templateStr,data){
    
    
        console.log("render函数被调用了")
        // 将模板字符串变成tokens数组
        var tokens = parseTemplateToTokens(templateStr)
    }
}
Output result:

Insert image description here

											图2.1

2.Create nestTokens method

Its function is to express the nested relationship between the generated Tokens, using the first-in-last-out principle of the stack.
Code part:
// 函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项

export default function nestTokens(tokens){
    
    
    // 结果数组
    var nestedTokens = []
    //  收集器,初始指向nestedTokens
    // 充分利用了引用类型的特点,这里的指向会改变,当遇到#的时候,会指向数组下标为2的位置,没有就创建
    var collector = nestedTokens
    // 栈结构,存放小tokens,栈顶(靠近端口的,最新进入的)的tokens数组中当前
    // 操作的这个tokens小数组
    var sections = []

    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] = []
                break
            case '/':
                // 出栈 pop()会返回刚刚弹出的项
                let section_pop = sections.pop()
                //改变收集器为栈结构队尾(队尾就是栈顶)那项的下标为2的数组
                collector = sections.length >0 ? sections[sections.length - 1][2] : nestedTokens
                break
            default:
                collector.push(token)
        }
    }


    console.log(nestedTokens)
    return nestedTokens
}
Test Results:

Insert image description here

Implementation ideas and code analysis:
  • First of all, due to the uncertainty of the nesting relationship of tokens, we don’t know how many layers are nested inside. For example, if students nest item.hobbies, there may still be many layers nested inside hobbies, so in terms of the nesting relationship, there is uncertainty. However, there is a data structure that can solve this kind of problem, and that is the stack. The stack has the characteristics of first in, last out, last in, first out. When it encounters #, it is pushed into the stack, and when it encounters /, it is popped out of the stack. This is why students are not good. The processing is actually the uncertainty of the internal number of layers. However, according to the characteristics of the stack, students are the first to be pushed onto the stack, that is, they are at the bottom of the stack. Therefore, after the push into the stack is completed, they must be processed last when popping out of the stack. , on the contrary, the innermost layer must be the simplest structure without nesting, and it is also the first data to be popped off the stack or processed. Then once this layer is solved, its upper layer will be the same. Processed, and so on, when students pop off the stack, his internal data has been processed, that is, it is determined.
    After the algorithm logic is determined, the code implementation begins. First of all, a for loop is definitely needed to find all the data in tokens, and an array of defined sections is needed, which is pushed from the end of the queue to the stack. Its function is to store the pushed array, such as Students will be pushed onto the stack first and then hobbies. So for the sections array, it is now a stack structure. The starting item of the array is the bottom of the stack, and the end of the array is the top of the stack. In this way, there is a stack structure. With the stack structure, we also need To improve the operations that need to be performed when loading and unloading the stack, in order to complete the collection of nested data, a new variable collector is defined here (this step is very subtle, because if only nestedTokens are used to collect data, too much needs to be done when # and / are encountered) The judgment operation is the if statement. The introduction of the collector variable makes the logic clearer). Its function is to collect variables, but unlike nestedTokens, it initially points to the nestedTokens variable through the reference type attribute. If # is not encountered, it is the same as nestedTokens. There is no difference (because its reference type has always been nestedTokens, so its push is equal to the push operation of nestedTokens), but when it encounters #, its pointer will change, and it will point to the latest array, or a smaller one. array,collector = token[2] = []As can be seen from the results Insert image description here, token[2]
    = [] is the array nested within students. It is created first and then uses collector=token[2] to point it to the latest array. At this time, the collection operation is for the token array of students. The item with subscript 2 is collected. If it is still ordinary data at this time, the collector keeps pushing the students' token[2] until it encounters a new #, that is, hobbies. Then the steps are repeated at this time. First, Hobbies is pushed into the stack, and then the collector is adjusted to point to the latest array, which is the token[2] item of hobbies, and then data is collected for it. If hobbies is the lowest level of nesting, then according to the characteristics of the stack, we know that hobbies will definitely do it first. Pop processing, through the tokens generated in the previous scanner, Figure 2.1, we can also see that hobbies are indeed popped out first. The pop symbol is /. When a / is encountered, it represents the end of a nested structure. When encountering hobbies' / structure, it proves that hobbies' data collection has been completed, and the collected data is stored in the array item of hobbies' token[2] as shown in the figure,

Insert image description here
In this way, the innermost hobbies data collection is completed, then this item needs to be popped off the stack, and the collector is pointed back to the token[2] item of students to continue collecting for it (the logic here is like this, start collecting data for nestedTokens, but When # is encountered, the collector goes to work for students, but encounters # again on the way, and it goes to work for hobbies again. Now hobbies is at the bottom. When it encounters the / logo of hobbies, it will pop out of hobbies. processing, then it will come back to continue working for students . After collecting the last remaining item, it will encounter the students' identification /. At this time, the students item will be stacked, and it will go back to help nestedTokens. After collecting the last item, the entire collection structure is displayed as shown above, which is the expected structure). What needs to be noted here is that after the collection of the students layer is completed, the stack sections are actually empty, so when the stack needs to be collector = sections.length >0 ? sections[sections.length - 1][2] : nestedTokenspopped Special processing is done. When the stack is empty, it means that it has reached the outermost layer, which is the nestedTokens layer. At this time, just let the collector point back to nestedTokens. Here, the collector and its reference relationship are cleverly used to collect data, so The advantage of this is that when encountering #, you only need to change the direction of the collector. There is no need to make too many judgments for nestedTokens. It will make a clean collection device. When encountering #, it will be pushed into the stack and/or popped out of the stack. The problem can be left to the collector to handle, thus completing the processing of the tokens nested structure.

3. Concatenate tokens and data into DOM string form:

1. Create renderTemplate function

Function:
By receiving tokens and data data, combining the two to return a string type DOM structure

import lookup from "./lookup"
import parseArray from "./parseArray";
export default function renderTemplate(tokens,data){
    
    
    console.log(data)
    // 结果字符串
    var resultStr = '';
    // 遍历tokens
    for(let i=0;i<tokens.length;i++){
    
    
        // 拿到每组token
        let token = tokens[i]
        // 类型判断
        if(token[0]=='text'){
    
    
            // 字符类型直接拼接
            resultStr +=token[1]
        }else if(token[0] =='name'){
    
    
            // resultStr +=data[token[1]]
            resultStr += lookup(data,token[1])
        }else if(token[0]=='#'){
    
    
            resultStr += parseArray(token,data)
        }
    }
    console.log(resultStr)
    return resultStr
}

Problem: Because resultStr +=data[token[1]] cannot recognize the syntax, when the data structure is multi-layer nested, as shown in Figure 2, token[1] is amn, obviously data['amn'] cannot be wrong Writing method, the result is shown in Figure 1. It is indeed undefined. In order to solve this problem, a lookup function needs to be created. Insert image description hereFigure 1
Insert image description here

													图2

2. Create a lookup function

Background:
Since it cannot be used by . syntax, the lookup function will return the bottom data when encountering a multi-layer structure. If it is ordinary data, it can directly return the result.

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

Result:
Just import and modify resultStr += lookup(data,token[1]) in the renderTemplate function. The result is as shown below, and the multi-layer structure can be identified.

Insert image description here

3. Create parseArray function

Its function is to deal with nested array problems.

Code:
import lookup from "./lookup"
import renderTemplate from "./renderTemplate"
// 处理数组,结合renderTemplate实现递归
// 这里的token并不是tokens,而是token=> ['#','students',[]]
// 简而言之就是这个token里面包含数组

// 重点:
// 这里需要递归调用renderTemplate函数,至于调用多少次和data数据有关
// 假设data的形式是:
// {
    
    
//     students : [
//         {"name":"某可","age":18,"sex":"男"},
//         {"name":"某鸿","age":18,"sex":"男"},
//         {"name":"小李","age":5,"sex":"女"}
//     ]
// }
// 那么就需要循环调用三次,因为数组的长度是三

export default function parseArray(token,data){
    
    
    console.log(token,data)
    // 拿到对于数据的名字 这里是students
    var v = lookup(data,token[1])
    var resultStr = ''
    // 遍历v数组,v一定是数组(原本可以跟Boolean值这里是简化版)
    for(let i=0;i<v.length;i++){
    
    
        // 这里和h函数的渲染有异曲同工之妙,token[2]里面存的又是类似于最外层这种tokens结构,v[i]就是data数据项
        // ★这里有个难点,如果是复杂属性例如上述students中第一项,外层是对象,内部是键值对属性表示
        // 那么v[i]传递数据肯定可以被拿到,因为这样模板就能通过name直接拿到里面的值如<p>姓名:{
    
    {name}}</p>,
        // 但是如果是数组["香蕉","苹果","梨子"],这样v[i]传递的数据就无法被拿到,因为无法识别模板中的.例如<li>{
    
    {.}}</li>
        // 其实就是因为数据是数组,而模板取数据通过属性名,数组没有属性名访问,一般是下标访问,所以用.符号代替
        // 所以这里需要取识别.符号,也就是简单数组数据类型
        // resultStr += renderTemplate(token[2],v[i])
        resultStr += renderTemplate(token[2],{
    
    
            // 如果是对象类型,证明已经有属性名了,那么直接拆分属性放到{}对象里面就好了
            ...v[i],
            // 如果是简单数组类型,那么就将.符号作为它的属性名,v[i]作为他的属性值,这样就能通过.拿到他的属性值了
            '.':v[i]
        })
        // 上面使用外层{}接受,将简单数组数据包装成{}类型,.符号就是他的属性名,属性值就是v[i]
    }
    return resultStr
}
Data and templates:

Insert image description here

operation result:

Insert image description here
Difficulties in the code:
First of all, the parseArray function is called in renderTemplate. In fact, parseArray does not need to be divided. The amount of code is not much, but the logic is difficult to understand. You can see it from the amount of comments in my code, so Extracted separately, the first idea used here is recursion. If you have seen the h function rendering virtual nodes, you must be very familiar with this. When the h function encounters the children attribute, it also implements the rendering of children by recursively calling the h function itself. For nodes, the implementation here is actually very simple, but it requires some difficulty. The function of renderTemplate is to convert tokens into DOM string splicing. It is easier to solve when encountering text and name attributes. Text is just a direct splicing of string forms, and The name attribute needs to get the data from the data data. We can also easily get the value through the lookup function. However, when encountering #, it means that there is a nested structure inside. In other words, the token also needs to be retrieved. It is treated as a token, which is very similar to the h function, so the renderTemplate function is recursively called here, and all splicing except # is completed and returned. In fact, in the final analysis, there must be a statement without #. The number of loop calls here depends on If the data type in data is like the sample students, then only three loop calls are needed to complete the creation of all templates. In fact, the effect here is the same as that of the common v-for in Vue. In the end, we only need to generate Assign the DOM string to the required innerHTML to display it on the page.

Reference video:
[Silicon Valley] Mustache template engine for Vue source code analysis
Part of the picture source:
[Silicon Valley] Mustache template engine for Vue source code analysis

Guess you like

Origin blog.csdn.net/weixin_54515240/article/details/130198895
Recommended