本文讨论 前端框架\模板中 文本插值的实现方案,本文将会主要以Vue框架作为参考讨论文本插值语法的具体实现和推导方案,并补充相关的技术细节。

文本插值

在Vue官网文档的第一部分( 声明式渲染 )我们可以看到下面一段描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:

<div id="app">
{{ message }}
</div>

var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
我们已经成功创建了第一个 Vue 应用!
看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。
现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?
打开你的浏览器的JavaScript控制台,并修改 app.message 的值,你将看到上例相应地更新。

在Vue官网的另一部分(模板语法-插值)说明了“ Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析 ”。

我们知道,在Vue框架中数据绑定的插值语法使用的是Mustache语法 (双大括号) ,而这篇短小的文章将简单讨论其内部的实现机制。

Class-实例的构建初步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 标签部分
<div id="app">
{{ message }}
</div>

# 引入框架文件
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

# 创建Vue实例
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})

在Vue框架中我们总是会通过上面的方式来创建并得到一个实例对象,在调用的时候我们传递了一个对象作为构造函数(class)的参数,在该对象中我们设置了挂载的标签(el属性)实例数据(data属性)等信息。这里,我们先提供一个 构造函数 或者是 Class 来模拟这个整体的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Class的写法 */
class Manager {
constructor(o) {
/* 根据传入的el来获取页面中挂载的标签 */
this.el = document.querySelector(o.el);

/* 把对象参数中的data成员(数据)添加到实例对象 */
/* 在访问的时候可以直接通过(new Manager()).xx访问 */
for (let key in o.data) {
this[key] = o.data[key];
}
}
}
/* 初始化:传入配置对象创建实例对象 */
let app = new Manager({
el: "#app",
data: {
message: "Hello 文顶顶!"
}
})

在开始的时候,[ 构造函数 \ Class ]的样子可能可能是像上面这样的,先尝试获取参数对象中el的值以获取实例在页面中挂载的标签,然后通过一个循环结构来把data中的数据都直接添加到实例对象,这种处理将允许我们直接以app.message的方式来操作数据。

数据和标签的渲染关系

设计出基本结构后,现在我们可以开始考虑如果需要把data中的数据渲染(绑定)到页面的标签,那该如何实现? 简单思考一秒钟后,我们似乎可以尝试以下的实践策略:

1
2
3
4
(1) 在初始化的操作中先获取挂载标签的属性节点(这很容易办到,使用innerHTML就可以)。
(2) 在innerHTML中寻找类类似于{{message}}的结构,如果找到那么抠出双括号中的字段-message
(3) 在实例对象中获取-message字段对应的value值,使用该值来替换{{message}}部分。
(!) 因为标签中可能存在多个插值代码,因此可能需要循环处理,在寻找插值代码的时候使用正则匹配或许会比较合适。

下面试着给出用正则来匹配标签内容并进行替换的核心代码,正则表达式的结果可以参考下面的注释,用于匹配 的特定结构,\s*表示可以允许存在空格,\\s表示对\进行转义处理,参数g用以表示应用全局匹配。

1
2
3
let reg = new RegExp(`{{2}\\s*msg\\s*}{2}`, "g");
/* /{{2}\s*msg\s*}{2}/g */
this.el.innerHTML = this.el.innerHTML.replace(reg, "文顶顶");

考虑到在参数对象的data中可能会有多个数据(键值对),且执行文本插值的时候某个数据可能会出现在标签的多个位置,因此需要通过循环的方式来检查 innerTTML 字段中每个数据的情况。我们可以通过 Object.keys()方法来获取所有的属性名(key的集合),然后遍历该数组并执行正则替换操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!-- 标签部分 -->
<div id="app">{{ message }}
<span>{{message}}</span>
<span>{{msg}}</span>
</div>
<!-- JS代码部分 -->
<script>
/* Class的写法 */
class Manager {
constructor(o) {
/* 根据传入的el来获取页面中挂载的标签 */
this.el = document.querySelector(o.el);

/* 把对象参数中的data成员(数据)添加到实例对象 */
/* 在访问的时候可以直接通过(new Manager()).xx访问 */
for (let key in o.data) {
this[key] = o.data[key];
}
/* 获取data数据中所有的key */
/* 根据data中的属性集合来遍历渲染页面中指定的内容 */
Object.keys(o.data).forEach(ele => {
let reg = new RegExp(`{{2}\\s*${ele}\\s*}{2}`, "g");
/* /{{2}\s*message\s*}{2}/g */
/* /{{2}\s*msg\s*}{2}/g */
console.log(this.el.innerHTML, reg);
this.el.innerHTML = this.el.innerHTML.replace(reg, this[ele]);
})
}
}

/* 初始化:传入配置对象创建实例对象 */
let app = new Manager({
el: "#app",
data: {
message: "文顶顶",
msg: "米桃儿"
}
})
</script>

当代码执行的时候,可以看到下面的效果。

至此,便简单了实现了数据-标签渲染的功能。如果数据发生变化后标签中对应的内容也要随之变化,这种数据驱动UI的结构最核心之处在于监听数据的变化并通知给UI视图,具体实现可以参考下一篇文章。