前端模板分析(一)
以一段简单的代码开始:
1 | $.ajax({/*...*/}).done(function(data) { |
这段代码就是实现根据数据拼接字符串,修改文档DOM的功能,很简单,大概每个前端程序员都写过,或至少感到熟悉。
但当这种代码大片大片出现时,由于逻辑和视图杂糅,拼接字符串天然就让代码难以阅读和维护。如何避免写这种代码,或者说一种解决方案——模板,就是本章的核心。
从苦逼的拼接字符串,到因为Backbone
认识underscore template
,到学习angular
而被双向绑定惊叹,再到了解React
等新技术,我对模板的认识也在不断加深。而且近来因为工作原因,从thinkphp模板
到django模板
的各种后端模板,应该说也算熟练使用。
从这一章开始,我打算用2~3篇文章分析前端模板,本章作为第一篇,专注于字符串模板;之后的一两篇会讲以DOM为基础的模板和两者的混合版。
字符串模板原理
如果网上搜一搜,前端模板引擎真不少,尤其在node出现之后,不少js模板引擎更是横跨前后端。虽然每个引擎的语法、解析方式、字符串拼接的实现可能各有不同,但关键的渲染原理仍然是动态执行javascript字符串。
模板使用和模板特征
首先假设有这样一段模板使用示例(以underscore为例):
1 | <div id='container'></div> |
可以看出,相比原始的字符串拼接,模板在可读性、可维护性和易用性上有极大提升。
前端模板引擎的3个特征:
前端模板一般有3种标签,分别是
- js代码执行标签(evaluate tag),标签内可以执行任意js代码,如
<% _.each(games, function(game) { %>
; - 插值标签(interpolate tag),输出变量的对应值,如
<%= game.id %>
; - 转义标签(escape tag),输出变量前先转义,如
<%- game.name %>
。注意:部分模板引擎可能省略这个标签。
- js代码执行标签(evaluate tag),标签内可以执行任意js代码,如
模板一般可以写在
script
标签里(即内联模板),但要注意改写type,可以是text/template
及类似的(不写或写成text/script
的话浏览器会当成脚本解析执行)。当然,用ajax加载等其它方法也可以,模板只是含有模板标签的字符串而已。模板引擎的接口基本一致:
template(tpl, data, options)
。如果模板和数据都传入,返回渲染好的html字符串;如果只传了模板,则返回一个模板函数,该函数接收data返回字符串。
以unserscore模板为例分析模板实现
废话不多说,以常用的unserscore内置模板讲解字符串模板的实现原理。
1 | // 3种标签,可配置更改 |
以上是underscore.js 1.7.0的前端模板相关代码。
通过上面的源码以及注释可以看出,模板引擎实现很简单,就是分析处理模板,然后构造一个函数动态执行js字符串。下面罗列几个需要注意的点:
- 模板分析的正则:(1)非贪婪匹配,(2)全局匹配
/g
,(3)组合正则时添加|$
。结合这3点,replace时就可以正确处理模板。其中非贪婪匹配保证每个标签是逐一处理(匹配<%str%>
而不是<%str%><%str%>
);全局则是让匹配进行到底,处理完整个模板;|$
保证模板一定被匹配,从而保证非标签内的字符串都被转义了。 with
的使用。with
在JavaScript中算是臭名昭著,但前端模板应该是普遍使用了with
,这里必然是有原因的。这里先不说,后面会专门一个小节来讲。
高效的字符串模板引擎
知道了字符串模板引擎是怎么回事后,我们更近一步,来谈谈众多前端模板引擎的性能比较,并谈谈高效的原因。
注意:这里的模板引擎仅仅指基于字符串的模板引擎,像angular之类的并不包括在内。公平而言,基于DOM的模板引擎(angular等)编译模板时会进行双向绑定,干的事远比字符串模板更多、更复杂,与字符串模板放在一起比较不合适。
参考artTemplate
项目的测试http://aui.github.io/artTemplate/test/test-speed.html
从图中可以看出,artTemplate
的速度在主流的几款前端模板中,速度突出。那么,比较上面分析的unsercore template
来看看artTemplate
做了哪些特殊处理/优化。
artTemplate模板引擎实现分析
去除一些细枝末节,直接分析artTemplate的核心实现。
1 | function compiler(source, options) { |
artTemplate的工具函数分析:
1 | // 字符串转义 ---> 处理html时使用 |
artTemplate的变量提取:
1 | // 静态分析模板变量 |
至此,artTemplate
的(核心)源码分析完毕,原理与underscore
模板基本一致。
underscore
模板处理逻辑很简单,基本就是:html转义后输出;插值和js代码原样输出。处理逻辑中只有一个简单的null/undefined
不输出。
artTemplate
模板处理更为复杂:
- html字符串根据设置来原样或转义输出;
- 逻辑字符串通用根据设置来原样或转义输出,但逻辑字符串中需要提取变量加到头部。
artTemplate
相比underscore template
在逻辑字符串上处理的不同也正是性能差异的关键:
underscore template
几乎原样输出,这种简单处理导致了with
的引进,或者说基于with
才可以这样简单处理;artTemplate
在逻辑字符串(js代码)中提取变量然后在顶部声明,弃用with
。
artTemplate模板引擎高效原因1——with
正如上面所说,抛弃with
是artTemplate高效的最大原因。那么with
为什么对性能影响如此之大?
首先确认with
的确会显著降低js性能:
1 | var funWith = function(data) { |
如上可以看出,with
对性能的影响巨大。
with
影响性能的两个原因:
1. 影响作用域链
在一个给定的执行环境中作用域链通常是不变的,但有两种情况会暂时增强作用域链(增加一级)。两种情形一个是try catch
,另一个就是with
。
with
会在当前作用域链的最底层添加一个对象(即变成作用域链上第一个对象),这个对象的所有属性都可以直接访问而不必通过.
操作符访问,这显然很方便。但作用域链上多出的这个对象会影响(hurt)本地变量解析,因为一旦with
语句执行,本地变量将位于作用域链上的第二个对象。
2. with
阻止js引擎的优化编译器的优化
V8引擎有两个不同的编译器:通用编译器(generic)和优化编译器(optimizing)。这意味着你的js代码被编译并直接以原生代码执行。那么,这是不是说你的代码会非常快?
错了,(js)代码被编译为原生代码本身并不意味着性能大大提升,编译只是消除了解释器开销,如果没有优化的话,(原生)代码可能仍然很慢。注意,这里的编译指通过通用编译器编译。
例如:js代码a + b
通过通用编译器编译后可能是这样子的:
1 | mov eax, a |
也就是说,它只是调用了运行时的函数。如果a
和b
是整数的话,可能是这样的:
1 | mov eax, a |
显然,这会极大地提高性能。通常情况下,通用编译器编译出来的就是前一种代码,而优化编译器编译出来的就是后一种代码。优化编译器编译后通常会有100X
的性能提升。
那么具体到我们的with
,with
语句所在的函数不会被优化编译器优化。这是因为with
块内是dynamical scoped
,不是通常的Lexical scoped
,这导致无法在编译时决定是绑定到哪个变量的(只能在运行时动态检测)。
所以,with
非常影响性能。
artTemplate模板引擎高效原因2——字符串相加
可以看到artTemplate中有array.push
和一般的+=
两种字符串相加方式。
很多人误以为数组 push 方法拼接字符串会比 += 快,但这仅仅是 IE6-8 的浏览器下。实测表明现代浏览器使用 += 会比数组 push 方法快,而在 v8 引擎中,使用 += 方式比数组拼接快 4.7 倍。所以 artTemplate 根据 javascript 引擎特性采用了两种不同的字符串拼接方式。
结束语
本篇博文中,我分析了underscore template
和artTemplate
两个模板引擎,借助这两个模板引擎,基于字符串的模板引擎的特征和原理应该清楚了。
我们可以看到artTemplate
对于underscore template
在性能上的优化,但模板非常耗费时间的一点——dom.innerHTML = compiledString
,基于字符串的模板引擎都没有涉及到(也无法涉及,因为基于字符串的模板从始至终都只是字符串的处理)。
另外,当数据变化时,字符串模板引擎只是不断的重复dom.innerHTML = compiledString
,而这里有很大的性能浪费,毕竟大部分情况不需要替换所有dom元素,可能只需要更新文本而已。而一旦替换全部dom元素,那么事件等等也都需要重新绑定。
为了解决这些问题,第二代模板——基于dom的模板如angularjs、avalonjs等等出现了,前端模板系列的下一篇的重点就将关注它们的实现原理。