乐趣区

关于vue.js:Vue源码解读一模板引擎

什么是模板引擎?

模板引擎是为了使用户界面与业务数据(内容)拆散而产生的,它能够生成特定格局的文档,用于网站的模板引擎就会生成一个规范的文档,就是将模板文件和数据通过模板引擎生成一个 HTML 代码。

本篇内容须要的 js 常量及 dom 构造
<body>
    <ul id="list"></ul>
    <script>
        var arr = [{name: '小明', age:23},
            {name: '小红', age:25},
            {name: '小强', age: 27}
        ]
    </script>
</body>

模板引擎的倒退

  1. 纯 dom 法 - 创立节点法
var list = document.getElementById('list')
for (var i = 0; i < arr.length; i++) {
    // 每遍历一项,都要用 DOM 办法创立 li 标签
    var oli = document.createElement('li');
    var hdDiv = document.createElement('div');
    hdDiv.className = 'hd';
    hdDiv.innerText = arr[i].name + '根本信息';
    var dbDiv = document.createElement('div');
    dbDiv.className = 'db';
    dbDiv.innerText = arr[i].name + '的根本信息';
    var p1 = document.createElement('p')
    p1.innerText = '姓名' + arr[i].name
    // 创立的节点是孤儿节点,必须上树能力被用户看见
    dbDiv.appendChild(p1)
    oli.appendChild(hdDiv)
    oli.appendChild(dbDiv)
    list.appendChild(oli)
}

这种形式内存开销大,繁冗简短。

  1. 数组 join()法 - 以字符串的视角追加内容
var list = document.getElementById('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> 性别:</p>',
        '</div>',
        '</li>'
    ].join('');
}
  1. ES6 反引号法 - 字符串自身能够换行,缩小了短字符串的个数
var list = document.getElementById('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> 性别:</p>
           </div>
       </li>
   `
}
  1. mustache 模板引擎
var templateStr = `
    {{#arr}}
    <li>
        <div class="hd">{{name}}的信息 </div>
        <div class="bd">
            <p> 姓名:{{name}}</p>
            <p> 年龄:{{age}}</p>
            <p> 性别:</p>
        </div>
    </li>
   {{/arr}}
`;
// render 接管两个参数:1. 模板字符串 templateStr;2. 数据 data
var domStr = Mustache.render(templateStr, data); // 最初生成 dom 字符串 domStr
// console.log(domStr)
var container = document.getElementById('list');
container.innerHTML = domStr;

mustache 模板字符串实现思路

Mustache 的底层外围机理tokens:js 的嵌套数组(模板字符串的 js 示意模式),且 Tokens 是“形象语法树”、“虚构节点”等等的思路起源。
Mustache 外围机理

stateDiagram-v2
    模板字符串 --> tokens
    tokens --> Dom 字符串
   数据 --> Dom 字符串        

一个 tokens 如下:

[["text", "<h1> 我买了一个"],
  ["name", "thing"],
  ["text", ",好"],
  ["name", "mood"],
  ["text", "啊 </h1>"]
]

这个二维数组的每一项就是一个 token。

mustache 具体实现思路如下:

  1. 筹备好模板字符串与数据,定义 render 渲染函数,传入模板字符串与数据,返回编译好的 Dom 字符串:
var htmlStr = render(templateStr, data)
document.getElementById('main').innerHTML = htmlStr

在 render 渲染函数外部,把模板字符串编译成 tokens 数组,再把 tokens 编译成 Dom 字符串:

render(templateStr, data) {
    // 调用 parseTempToToken 函数,让模板字符串变成 tokens 数组
    var tokens = parseTempToToken(templateStr);
    // 调用 renderTemplate 函数,让 tokens 数组变为 dom 字符串
    var domStr = renderTemplate(tokens, data)
    console.log('domStr:\n', domStr)
    return domStr;
}
  1. 定义 parseTempToToken,将模板字符串变为 tokens 数组

    /**
     * 将模板字符串变为 tokens 数组
    */
    export default function parseTempToToken (templateStr) {let tokens = [];
     // 创立扫描器
     let scanner = new Scanner(templateStr);
     let words;
     // 让扫描器工作
     while (!scanner.eos()) {
         // 收集开始标记呈现之前的文字
         words = scanner.scanUtil('{{');
         if (words !== '') {
             // 尝试写一下去掉空格,智能判断是一般文字的空格,还是标签中的空格
             // 标签中的空格不能去掉比方 <div class="box"> 不能去掉 class 后面的空格
             let isInJJH = false;
             // 空白字符串
             var _words = '';
             for (let i = 0; i < words.length; i++) {
                 // 判断是否在标签内
                 if (words[i] === '<') {isInJJH = true} else if (words[i] === '>') {isInJJH = false}
                 if (!/\s/.test(words[i])) {
                     // 如果这项不是空格,拼接上
                     _words += words[i]
                 } else {
                     // 如果这项是空格, 只有在标签里才保留空格
                     if(isInJJH) {_words += words[i]
                     }
                 }
             }
             // 存起来,去掉空格
             tokens.push(['text', _words]);
         }
         // 过双括号{{scanner.scan('{{')
         // 收集开始标记呈现之前的文字
         words = scanner.scanUtil('}}');
         if (words !== '') {// 这个 words 就是 {{}} 两头的内容,判断一下首字符
             if (words[0] === '#') {
                 // 存起来,从下标为 1 的项开始存,因为下标为 0 的项是#
                 tokens.push(['#', words.substring(1)])
             } else if (words[0] === '/') {
                 // 存起来,从下标为 1 的项开始存,因为下标为 0 的项是 /
                 tokens.push(['/', words.substring(1)])
             } else {
                 // 存起来
                 tokens.push(['name', words]);
             }
             // 存起来
             // tokens.push(['name', words]);
         }
         // 过双括号{{scanner.scan('}}')
     }
     // 返回折叠的 tokens
     return nestTokens(tokens);
    }

    1)定义扫描器,次要体现在对模板字符串遍历时,指针的挪动

    /**
     * 扫描器类
    */
    export default class Scanner {constructor(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) {
         // 记录一下执行本办法时候的 pos 的值
         const pos_backup = this.pos;
         // 当尾巴的结尾不是 stopTag 的时候,就阐明还没扫描到 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.tail === '';}
    }

    parseTempToToken 函数中调用扫描器 scanner 类的 scanUtil 办法(收集开始标记之前的文字)、scan 办法 (过滤双括号{{,}}),eos 办法(判断是否宰割到字符尾部),最初返回宰割好的平级 tokens。
    2) 定义 nestTokens 函数,将上一步扫描完生成的平级 tokens 解决为嵌套 tokens

/**
 * 函数的性能是折叠 tokens,将 #和 / 之间的 tokens 可能整合起来,作为它的下标为 3 的项
*/
export default function nestTokens(tokens) {
    // 后果数组
    let nestTokens = [];
    // 栈构造,寄存小 tokens,栈顶(凑近端口的,最新进入的)的 tokens 数组中以后操作的这个 tokens 小数组
    var sections = [];
    // 收集器,初始指向 nestTokens, 援用类型值,所以指向的是同一个数组
    // 收集器的指向会变动,当遇见 #的时候,收集器会指向这个 token 的下标为 2 的新数组
    let collector = nestTokens;
    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] : nestTokens;
                break;
           default:
               collector.push(token)
               break;
        }
    }
    return nestTokens;
}

在 nestTokens 通过设置收集器 collector, 并在循环中判断 token 字符中是否有“#”来判断以后级有没有下一级,有的话将收集器指向本级的下一级,通过‘/’判断本机循环完结并出栈;如果都没有那就是本级的平级,压入栈顶。
至此,模板字符串的 tokens 组装结束


  1. 定义 renderTemplate 函数,将 tokens 注入数据(把 tokens 数组变为 dom 字符串)
/**
 * 函数的性能是让 tokens 数组变为 dom 字符串
*/
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') {
            // 如果是 name 类型,那么就间接应用它的值,当然要用 lookup
            // 避免这里是 'a.b.b' 有逗号的模式
            resultStr += lookup(data, token[1])
        } else if (token[0] === '#') {resultStr += parseArray(token, data)
        }
    }
    return resultStr
}
  1. renderTemplate 中调用 lookup 函数,在 dataObj 对象中,寻找用间断点符号的 keyName 属性,也就是读取嵌套的简单数据类型中的值。
/**
 * 性能是能够在 dataObj 对象中,寻找用间断点符号的 keyName 属性,比方,dataObj 是
 * {
 *  a: {
 *      b: {
 *        c: 100
 *      }
 *  }
 * }
 * 那么 lookup(dataObj, 'a.b.b') 后果就是 100
*/
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];
}
  1. 在曾经确定有上级的 token 中递归调用 parseArray()办法,遍历数据,再在 renderTemplate 中填充。
/**
 * 解决数组,联合 renderTemplate 实现递归
 * 这个函数收的参数是 token! 而不是 tokens!
 * token 就是一个简略的['#', 'student', []]
 * 这个函数要递归调用 renderTemplate 函数,调用多少次由 data 决定
 * 比方 data 的模式是这样的:* {
        students: [{name: '小明', age: 12, hobbies: ['游泳', '羽毛球']},
            {name: '小红', hobbies: ['足球', '篮球', '羽毛球']}
        ]
    }
    那么 parseArray()函数就要递归调用 renderTemplate()三次,数组长度是 3
*/
export default function parseArray(token, data) {
    // 失去整体数据 data 中这个数组要应用的局部
    var v = lookup(data, token[1]);
    // 后果字符串
    var resultStr = '';
    // 留神,上面这个循环可能是整个包中最难思考的一个循环
    // 它是遍历数据,而不是遍历 tokens
    for (let i = 0; i < v.length; i++) {
        // 这里须要补一个“.”属性
        resultStr += renderTemplate(token[2], {...v[i],
            '.': v[i]
        })
    }
    return resultStr;
}

到此 mustache 实现模板字符串的整个过程就全副完结了,回到最后,渲染函数 render 里边的失去的 tokens,domStr 如下图:

渲染到页面上:

退出移动版