IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Vue 源码学习 -> 正文阅读

[JavaScript知识库]Vue 源码学习

Vue 源码 – mustache 模版引擎

1. 什么是模版引擎

  • 数据 --> 视图
    • 纯 DOM 法
    • 数组 join 法
    • ES6 反引号
    • 模版引擎
  • 模版引擎是将数据变为视图最优雅的解决方案
// 数据
[
	{"name":"张三", "age":18},
	{"name":"李四", "age":20},
	{"name":"王五", "age":22}
]

// 视图
<ul>
	<li>
		<div class="name">姓名:张三</div>
		<div class="age">年龄:18</div>
	</li>
	<li>
		<div class="name">姓名:李四</div>
		<div class="age">年龄:20</div>
	</li>
	<li>
		<div class="name">姓名:王五</div>
		<div class="age">年龄:22</div>
	</li>
</ul>

// v-for

2. mustache 基本使用

2.1 mustache 库简介

  • 官方 git: https://github.com/janl/mustache.js
  • mustache 是最早的模板引擎库,它的底层实现原理在当时是非常有创造性的、轰动性的,为后续模板引擎的发展提供了崭新的思路

2.2 mustache 库基本使用

2.2.1 引入 mustache 库

2.2.2 循环对象数组

  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
			// 模版语法
		    {{#arr}} // 从#开始循环arr这个数组,name、sex、age都是arr数组里的值
		        <li>   
		            <div class="name">姓名:{{name}}</div>
					<div class="age">年龄:{{age}}</div>
		        </li>
		    {{/arr}}
		</ul>
	`

	var data = {
		arr: [
			{"name":"张三", "age":18},
			{"name":"李四", "age":20},
			{"name":"王五", "age":22}
		]
	}
	
	/*
	  Mustance 全局变量
	  render 方法(层递)
	  templateStr 模版字符串
	  data 数据
	*/
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

2.2.3 不循环

  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<h1>我是{{name}},今年{{age}}岁</h1>
	`

	var data = {
		name: '张三',
        age: 18
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

2.2.4 循环简单数组

  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
            {{#arr}}
                <li>{{.}}</li>    
            {{/arr}}
        </ul>
	`

	var data = {
		arr: ['A', 'B', 'C']
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

2.2.5 数组嵌套

  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		<ul>
            {{#arr}}
                <li>
                    {{name}}的爱好是:
                    <ol>
                        {{#hobbies}} 
                            <li>{{.}}</li>
                        {{/hobbies}}
                    </ol>
                </li>    
            {{/arr}}
        </ul>
	`

	var data = {
		arr: [
        	{'name': '小明', 'age': 12, 'hobbies': ['游泳', '羽毛球']},
            {'name': '小红', 'age': 11, 'hobbies': ['编程', '写作文', '看报纸']},
            {'name': '小强', 'age': 13, 'hobbies': ['打台球']},
        ]
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

2.2.6 布尔值

  • Mustache.render(templateStr, data)
<div id="container"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/mustache.js/4.2.0/mustache.js"></script>
<script>
	var templateStr = `
		{{#m}}
            <h1>你好</h1>
        {{/m}}
	`

	var data = {
		m: false
	}
	
	var domStr = Mustache.render(templateStr, data)
	var container = document.getElementById('container');
    container.innerHTML = domStr;
</script>

3. mustache 底层核心原理

3.1 正则表达式思路

  • 在简单情况下,可以用正则表达式实现
  • 但当情况复杂时,正则表达式的思路就不行了
var templateStr = `
	<h1>我是{{name}},今年{{age}}岁</h1>
`

var data = {
	name: '张三',
    age: 18
}

// 最简单的模板引擎的实现机理,利用的是正则表达式中的replace()方法。
// replace()的第二个参数可以是一个函数,这个函数提供捕获的东西的参数,就是$1
// 结合data对象,即可进行智能的替换
function render(templateStr, data) {
    return templateStr.replace(/\{\{(\w+)\}\}/g, function (findStr, $1) {
        return data[$1];
    });
}

var result = render(templateStr, data);
console.log(result);

3.2 mustache 库的原理

请添加图片描述

  • Mustache 库底层重点做了两件事
    • 将模版字符串编译为 tokens 形式
    • 将 tokens 结合数据,解析为 DOM 字符串

3.2.1 什么是 tokens

  • tokens 是一个 JS 的嵌套数组
// 模版字符串
<h1>我是{{name}},今年{{age}}岁</h1>

// tokens
[
	["text", "<h1>我是"],
	["name", "name"],
	["text", ",今年"],
	["name", "age"],
	["text", "岁</h1>"]
]

3.2.2 循环情况下的 tokens

  • 当模版字符串中有循环存在时,它将被编译为嵌套更深的 tokens
// 模版字符串
<div>
	<ul>
		{{#arr}}
		<li>{{.}}</li>
		{{/arr}}
	</ul>
</div>

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

3.2.3 双重循环情况下的 tokens

  • 循环是双重的,那么 tokens 会更深一层
// 模版字符串
<div>
	<ol>
	{{#students}}
	<li>
		学生
		<ol>
			{{#hobbies}}
			<li>{{.}}</li>
			{{/hobbies}}
		</ol>
	</li>
	{{/students}}
	</ol>
</div>

// tokens
[ 
	["text", "<div><ol>"],
	["#", "students", [ 
		["text", "<li>学生"],
		["name", "name"],
		["text", "的爱好是<ol>"],
		["#", "hobbies", [ 
			["text", "<li>"],
			["name", "."],
			["text", "</li>"],
		]],
		["text", "</ol></li>"],
	]],
	["text", "</ol></div>"] 
]

4. 手写 mustache 库

4.1 构建项目

4.1.1 使用 webpack 和 webpack-dev-server 构建
  • Mustache 官方库使用 rollup 进行模块化打包
  • 但是我这里使用 webpack ( webpack-dev-server ) 进行模块化打包
    • webpack 能更方便的在浏览器中实时调用程序 ( 相比于 nodejs 控制台,浏览器控制台更好用 )
  • 生成库是 UMD 的,这意味着它可以同时在 nodejs 环境中使用,也可以在浏览器环境中使用
4.1.2 webpack.config.js 文件
const path = require('path');

module.exports = {
    // 开发模式
	mode: 'development',
	// 入口文件
	entry: './index.js',
	// 打包文件 
	output: {
		filename: 'bundle.js'
	},
	// 配置 webpack-dev-server
	devServer: {
		// 静态文件根目录 
		contentBase: path.join( dirname, "www"), 
		compress: false, // 不压缩
		port: 8080, // 端口号
		// 虚拟打包的路径,bundle.js文件没有真正的生成
		publicPath: "/xuni/"
	}
};

4.2 整体结构

  • index.html
<body>
    <div id="container"></div>

    <script src="/xuni/bundle.js"></script>

    <script>
        // 模板字符串
        var templateStr = `
            <div>
                <ul>
                    {{#students}}
                    <li class="myli">
                        学生{{name}}的爱好是
                        <ol>
                            {{#hobbies}}
                            <li>{{.}}</li>
                            {{/hobbies}}
                        </ol>
                    </li>
                    {{/students}}
                </ul>
            </div>
        `;

        // 数据
        var data = {
            students: [
                { 'name': '小明', 'hobbies': ['编程', '游泳'] },
                { 'name': '小红', 'hobbies': ['看书', '弹琴', '画画'] },
                { 'name': '小强', 'hobbies': ['锻炼'] }
            ]
        };

        // 调用render
        var domStr = test_TemplateEngine.render(templateStr, data);
        console.log(domStr);

        // 渲染上树
        var container = document.getElementById('container');
        container.innerHTML = domStr;
    </script>
</body>
  • index.js
// 全局提供 test_TemplateEngine 对象
window.test_TemplateEngine = {
    // 渲染方法
    render(templateStr, data) {
        // 调用 parseTemplateToTokens 函数,让模板字符串能够变为 tokens 数组
        var tokens = parseTemplateToTokens(templateStr);
        // 调用 renderTemplate 函数,让 tokens 数组变为 dom 字符串
        var domStr = renderTemplate(tokens, data);
        
        return domStr;
    }
};

4.3 具体方法

4.3.1 如何将模版字符串变为 tokens
  • Scanner 扫描类
    • 扫描模版字符串,判断类型
/* 
    扫描器类
*/
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);
    }

    // 指针是否已经到头,返回布尔值。end of string
    eos() {
        return this.pos >= this.templateStr.length;
    }
};
  • tokens 嵌套
    • 利用数据结构–栈
    • 扫描到 # 号进栈,扫描到 / 号出栈
  • nestTokens.js
/* 
    函数的功能是折叠tokens,将#和/之间的tokens能够整合起来,作为它的下标为3的项
*/
export default function nestTokens(tokens) {
    // 结果数组
    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] = [];
                break;
            case '/':
                // 出栈。pop()会返回刚刚弹出的项
                sections.pop();
                // 改变收集器为栈结构队尾(队尾是栈顶)那项的下标为2的数组
                collector = sections.length > 0 ? sections[sections.length - 1][2] : nestedTokens;
                break;
            default:
                // 甭管当前的collector是谁,可能是结果nestedTokens,也可能是某个token的下标为2的数组,甭管是谁,推入collctor即可
                collector.push(token);
        }
    }

    return nestedTokens;
};
  • parseTemplateToTokens.js
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 != '') {
            // 去掉空格,智能判断是普通文字的空格,还是标签中的空格
            // 标签中的空格不能去掉,比如<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 += ' ';
                    }
                }
            }
            // 存起来,去掉空格
            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]);
            }
        }
        // 过双大括号
        scanner.scan('}}');
    }

    // 返回折叠收集的tokens
    return nestTokens(tokens);
}
4.3.2 如何将 tokens 数组变为 DOM 字符串
  • # 标记的 tokens,需要递归处理下标为2的小数组
  • 注意
    • JS 不认识点符号
  • lookup.js
/* 
    功能是可以在dataObj对象中,寻找用连续点符号的keyName属性
    比如,dataObj是
    {
        a: {
            b: {
                c: 100
            }
        }
    }
    那么lookup(dataObj, 'a.b.c')结果就是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];
};
  • 递归
  • parseArray.js
import lookup from './lookup.js';
import renderTemplate from './renderTemplate.js';

/* 
    处理数组,结合renderTemplate实现递归
    注意,这个函数收的参数是一个token,而不是多个token组成的tokens
    
    这个函数要递归调用renderTemplate函数,调用的次数由data决定
    比如data的形式是这样的:
    {
        students: [
            { 'name': '小明', 'hobbies': ['游泳', '健身'] },
            { 'name': '小红', 'hobbies': ['足球', '蓝球', '羽毛球'] },
            { 'name': '小强', 'hobbies': ['吃饭', '睡觉'] },
        ]
    };
    那么parseArray()函数就要递归调用renderTemplate函数3次,因为数组长度是3
*/

export default function parseArray(token, data) {
    // 得到整体数据data中这个数组要使用的部分
    var v = lookup(data, token[1]);
    // 结果字符串
    var resultStr = '';
    // 遍历v数组,v一定是数组
    // 注意,下面这个循环可能是整个包中最难思考的一个循环
    // 它是遍历数据,而不是遍历tokens。数组中的数据有几条,就要遍历几条。
    for(let i = 0 ; i < v.length; i++) {
        // 这里要补一个“.”属性
        // 拼接
        resultStr += renderTemplate(token[2], {
            ...v[i],
            '.': v[i]
        });
    }
    return resultStr;
};
  • renderTemplate.js
import lookup from './lookup.js';
import parseArray from './parseArray.js';
/* 
    函数的功能是让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.c”有逗号的形式
            resultStr += lookup(data, token[1]);
        } else if (token[0] == '#') {
            resultStr += parseArray(token, data);
        }
    }

    return resultStr;
}
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-16 11:38:44  更:2021-08-16 11:40:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/26 16:47:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计