模板引擎

模板引擎不属于特定技术领域,是为了使用户界面与业务数据(内容)分离而产生的,是跨领域跨平台的概念。严格的模板引擎的定义是,输入模板字符串+数据,得到渲染过的字符串(页面)。

后端服务器的资源是有限的,并且对数据的处理是随着用户数量的增加而叠加的,用户的每一次操作,页面渲染都是在消耗服务器资源,少量的用户操作或许不会导致服务器卡顿,但是当出现成千上万甚至更多的用户时,可能仅是网络请求就会让服务器无响应甚至宕机(参照春运)。而如果将页面的渲染放在用户端(前端),用户只有一个,几十毫秒的渲染时间跟请求延迟比起来根本算不上瓶颈,所以既可以提高用户的体验,同时也减轻了服务器的压力。

通常我们将渲染方法设计为render(),参数就是模板路径和数据。render()我们可以将其看成是一个约定接口,接受相同参数,最后返回HTML片段。这样的方法我们都视作实现了这个接口。
如:

形成模板技术的也就如下4个要素。
❑ 模板语言。
❑ 包含模板语言的模板文件。
❑ 拥有动态数据的数据对象。
❑ 模板引擎。

实现上,从正则替换的方式到拼写字符串直接输入,再到AST解析,存在各种输出页面内容的方式,但从定义上来说都是差不多的。

前端模板引擎分为两种,基于字符串的模板引擎和基于JavaScript的模板引擎。

模板引擎主要有以下几个步骤。
❑ 语法分解。提取出普通字符串和表达式,这个过程通常用正则表达式匹配出来
❑ 处理表达式。将标签表达式转换成普通的语言表达式。
❑ 生成待执行的语句。
❑ 与数据一起执行,生成最终字符串。

基于字符串的模板引擎

就是通过字符串替换的方式,来渲染出HTML,再将HTML插入DOM节点中,其代表性框架有Mustache和Handlebars.js。

从软件架构上来看,Mustache提供了多语言的版本,即官方提供了诸如Python、Java、Ruby等语言的模板引擎。因为前后台共用这些模板,它可以使得后台渲染变得相当容易。只需要读取对应的模板文件,就可以将Model填充到数据中。

其基本原理是,全局正则匹配模板关键字,再从传入的data中找到是否有对应的值,如data中存在对象,则使用该值进行替换,否则该值为空。

基于字符串的模板引擎在更新DOM的时候会更新所有DOM节点,这时浏览器需要重新渲染所有的节点。当我们拥有大量的HTML元素或DOM操作时,如果每次都刷新整个DOM节点,显然是不合适的。

基于JavaScript的模板引擎

基于JavaScript的模板引擎的表现形式是,将模板转义为JavaScript,在执行的过程中,再动态更新所需要的DOM节点。相应的逻辑如下:
(1)将模板编译为某种DSL(领域特别语言),比如HyperScript或者JavaScript对象(代码+数据),创建虚拟的树(AST树)
(2)在使用时,通过这个虚拟树来创建一个DOM节点。
(3)(可选)当发生变更时,通过DOM Diff算法来替换对应的修改结点。

它和基于字符串的模板引擎的区别主要在于第三点,是否全局替换DOM节点。比如Virtual DOM,就是带一个Dom Diff的JavaScript模板引擎,在进行大量DOM操作的时候,由于只对变化的DOM进行替换,可以提高前端应用的性能。

Vue的render实现

template 模板 通过 Compile 编译 得到 render函数。

compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

其实就是Vnode+data

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
<div id=”app“>
<p>{{price}}</p>
</div>

<script>
/**
* _c : 创建dom标签
* _v : 创建文本节点
* _s : toString
*/
var vm = new Vue({
el: ’\#app‘,
data: {
price: 100
}
})
// 这个模板最终生成的函数是下面这个
// 以下是手写的 render 函数
function render() {
with(this) { // this 就是 vm
return _c(
’div‘,
{
attrs: {’id‘: ’app‘}
},
[
_c(’p‘, [_v(_s(price))])
]
)
}

_c即$createElement 的实现(如何创建Vnode):

vue的整个实现流程简易说明
第一步:解析模板成 render 函数
第二步:响应式开始监听
第三步:首次渲染,显示页面,且绑定依赖
第四步:data 属性变化,触发 rerender

  1. 把模板解析为 render 函数

    运用 with,模板中的所有信息都被 render 函数包含;模板中用到的 data 中的属性,都变成了 JS 变量;模板中的 v-model v-for v-on 都变成了 JS 逻辑;render 函数返回 vnode

  2. 响应式开始监听

    Object.defineProperty 将data 的属性代理到 vm 上

  3. 首次渲染,显示页面且绑定依赖

    初次渲染,执行 updateComponent, 执行vm._render();执行 render 函数,会访问到 data 下的数据,被响应式的 get 方法监听到;执行 updateComponent,会走到 vdom 的 patch 方法;patch 将 vnode 渲染成 DOM,初次渲染完成

  4. data 属性变化,触发 rerender

    修改属性,被响应式的 set 监听到;set 中执行 updateComponent;updateComponent 重新执行 vm._render();生成的 vnode 和 prevVnode ,通过 patch 进行对比;渲染到 html 中

一个简明的js实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  var TemplateEngine = function(html, options) {
var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = ’var r=[];\n‘, cursor = 0;
var add = function(line, js) {
js? (code += line.match(reExp) ? line + ’\n‘ : ’r.push(‘ + line + ’);\n‘) :
(code += line != ’‘ ? ’r.push(”‘ + line.replace(/“/g, ’\\”‘) + ’“);\n‘ : ’‘);
return add;
}
while(match = re.exec(html)) {
add(html.slice(cursor, match.index))(match[1], true);
cursor = match.index + match[0].length;
}
add(html.substr(cursor, html.length - cursor));
code += ’return r.join(”“);‘;
return new Function(code.replace(/[\r\t\n]/g, ’‘)).apply(options);
}

从编译角度理解v-if和v-show的区别

v-if编译成了三元运算符

同时还可以解释为什么连用时v-for比v-if优先级高。所以最好在外面嵌套一个template,把v-if放在外层。或者用计算属性先过滤

v-show编译成了指令