什么是JavaScript引擎?JavaScript引擎实现的原理是什么?javascript 模板引擎artTemplate介绍。
随着 web 发展,前端应用变得越来越复杂,此时 javascript 被寄予了更大的期望,与此同时 javascript MVC 思想也开始流行起来。javascript 模板引擎作为数据与界面分离工作中最重要一环,越来越受开发者关注。本文将用最简单的示例代码描述现有的 javascript 模板引擎的原理,包括新一代 javascript 模板引擎 artTemplate 的特性实现原理,欢迎共同探讨。
什么是JavaScript引擎
其实在网站开发中模板还是很常见的一种技术,比如ASP.NET的Master Page等,但这些模板都是基于服务器的,JavaScript模板引擎是为了解决我们在前端写出形如这样的拼html的语句。
var html='<ul>'; for(var i=0;i<users.length;i++){ html+='<li><a href=">'+users[i].url+'">'+users[i].name+'</a>'; } html+='</ul>'; document.getElementById('results').innerHTML=html;
上面的代码我们一看就知道是在拼html,但这样的代码如果长了,相信谁看了都不会再想看第二遍,说不定过几个月让你自己来看都不知道自己写的是什么了,但是如果我们有这样一个模板
<ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul>
看了很容易就明白开发者希望得到的是这样的html
<ul> <li><a href="url1">name1</a></li> <li><a href="url2">name2</a></li> <li><a href="rul3">name3</a></li> </ul>
简单来说,JavaScript模板引擎就是帮我们把带有JavaScript代码的伪html语句翻译为html的东西。
javascript 模板引擎基本原理
虽然每个引擎从模板语法、语法解析、变量赋值、字符串拼接的实现方式各有所不同,但关键的渲染原理仍然是动态执行 javascript 字符串。
关于动态执行 javascript 字符串,本文以一段模板代码举例:
<ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul>
想得到预期html字符串,我们必须设法让模板内部的javascript变量置换、javaScript语句执行,也就是把JavaScript代码剥离出来执行,把其它html语句拼接为一个字符串。
var p=[]; p.push('<ul>'); for(var i=0;i<users.length;i++){ //javascript语句执行 p.push('<li><a href="'); //html语句拼接 p.push(users[i].url); //javascript变量置换后拼接 p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');
最后得到的数组join一下就是我们希望得到的字符串了,首先需要取到模板内的字符串,我们可以把模板放到一个script标签里(防止在页面显示出来),换成我们特定的类型。
<script type="text/html" id="user_tmpl"> <ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li> <a href="<%=users[i].url%>"><%=users[i].name%></a> </li> <% } %> </ul> </script>
这样就可以通过 document.getElementById(str).innerHTML 来获取模版内字符串了,然后我们应用一些简单的法则处理一下模板内字符串。
<%=xxx%> → ');p.push(xxx);p.push(' <% → '); %> → p.push('
这样我们就可以得到这样的结构,看起来就已经很接近结果了
p.push('<ul>'); for(var i=0;i<users.length;i++){ p.push('<li><a href="'); p.push(users[i].url); p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');
这里我们要做的只有三步:
1.把<%=xxx%> 替换为 ');p.push(xxx);p.push('
html=html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('");
2.把<%替换为 ');
html=html.replace(/<%/g,"');");
3.把%> 替换为 p.push('
html=html.replace(/%>/g,"p.push('");
我们再把结果用p.push(' 和 '); 包裹起来就可以看到初步效果了
function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[]; p.push('" +html.replace(/<%=(.*?)%>/g,"');p.push(xxx);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +" ');"; }
这样我们就把html模版内容替换成了这样的一个字符串
var result=" var p=[]; p.push('<ul>'); for(var i=0;i<users.length;i++){ p.push('<li><a href="'); p.push(users[i].url); p.push('">'); p.push(users[i].name); p.push('</a></li>'); } p.push('</ul>');"
貌似得到结果了,但我们得到的是字符串,我们预期的是这个字符串执行的结果,很多同学会想到使用eval就可以让字符串变成JavaScript语句执行,但是Jonh使用了另外一种方式——创建function,我们知道除了常用使用function关键字创建一个function
function fn(data){ console.log(data); }
还可以使用Function构造函数来创建一个function
var fn = new Function(arg1, arg2, ..., argN, function_body)
在上面的形式中,每个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码),使用这种方式可以动态(方法体是动态生成的,提前不知道,当然这样做会有效率问题)创建一个方法,也就是说我们还可以使用刚才拼出来的javascript字符串动态创建一个函数。
function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[];with(obj){p.push('" +html.replace(/[\r\n\t]/g," ") .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +"');}return p.join('');"; var fn=new Function("obj",result); return fn(data); }
例子
<!DOCTYPE html> <html> <head> <title>模板</title> </head> <body> <!--结果容器--> <div id="results"></div> <!--模板--> <script type="text/html" id="user_tmpl"> <ul> <% for ( var i = 0; i < users.length; i++ ) { %> <li><a href="<%=users[i].url%>"><%=users[i].name%></a></li> <% } %> </ul> </script> <script type="text/javascript"> //模板数据 var results = document.getElementById("results"); var users=[ {"name":"列表1", "url":"http://www.hejingzong.cn"}, {"name":"列表2", "url":"http://www.hejingzong.cn"}, {"name":"列表3", "url":"http://www.hejingzong.cn"} ]; //模板函数 function tmpl(id,data){ var html=document.getElementById(id).innerHTML; var result="var p=[];with(obj){p.push('" +html.replace(/[\r\n\t]/g," ") .replace(/<%=(.*?)%>/g,"');p.push($1);p.push('") .replace(/<%/g,"');") .replace(/%>/g,"p.push('") +"');}return p.join('');"; var fn=new Function("obj",result); return fn(data); } //输出结果 results.innerHTML = tmpl("user_tmpl", users); </script> </body> </html>
最后会得到如下的结果:
不足之处
在上面的方法中,模板变量赋值采用了 with 语句,字符串拼接采用数组的 push 方法以提升在 IE6、7 下的性能,jQuery 作者 john 开发的微型模板引擎 tmpl 是这种方式的典型代表,参见: http://ejohn.org/blog/javascript-micro-templating/
由原理实现可见,传统 javascript 模板引擎中留下两个待解决的问题:
1、性能:模板引擎渲染的时候依赖 Function 构造器实现,Function 与 eval、setTimeout、setInterval 一样,提供了使用文本访问 javascript 解析引擎的方法,但这样执行 javascript 的性能非常低下。
2、调试:由于是动态执行字符串,若遇到错误调试器无法捕获错误源,导致模板 BUG 调试变得异常痛苦。在没有进行容错的引擎中,局部模板若因为数据异常甚至可以导致整个应用崩溃,随着模板的数目增加,维护成本将剧增。
artTemplate 介绍
artTemplate 是新一代 javascript 模板引擎,它采用预编译方式让性能有了质的飞跃,并且充分利用 javascript 引擎特性,使得其性能无论在前端还是后端都有极其出色的表现。在 chrome 下渲染效率测试中分别是知名引擎 Mustache 与 micro tmpl 的 25 、 32 倍。
速度对比
除了性能优势外,调试功能也值得一提。模板调试器可以精确定位到引发渲染错误的模板语句,解决了编写模板过程中无法调试的痛苦,让开发变得高效,也避免了因为单个模板出错导致整个应用崩溃的情况发生。
artTemplate 高效的秘密
1、预编译
在上述模板引擎实现原理中,因为要对模板变量进行赋值,所以每次渲染都需要动态编译 javascript 字符串完成变量赋值。而 artTemplate 的编译赋值过程却是在渲染之前完成的,这种方式称之为“预编译”。artTemplate 模板编译器会根据一些简单的规则提取好所有模板变量,声明在渲染函数头部,这个函数类似:
var render=function($data){ var content=$data.count,$out=''; $out += '<h3>'; if(typeof count === 'string'){ $out += count; } $out += '</h3>'; return $out; }; render({content:'逍遥乐网博客'});
这个自动生成的函数就如同一个手工编写的 javascript 函数一样,同等的执行次数下无论 CPU 还是内存占用都有显著减少,性能近乎极限。
值得一提的是:artTemplate 很多特性都基于预编译实现,如沙箱规范与自定义语法等。
2、更快的字符串相加方式
很多人误以为数组 push 方法拼接字符串会比 += 快,要知道这仅仅是 IE6-8 的浏览器下。实测表明现代浏览器使用 += 会比数组 push 方法快,而在 v8 引擎中,使用 += 方式比数组拼接快 4.7 倍。所以 artTemplate 根据 javascript 引擎特性采用了两种不同的字符串拼接方式。
下载地址:
https://github.com/aui/artTemplate/
在线预览:
http://aui.github.com/artTemplate/
相关链接:
Juicer —— 一个Javascript模板引擎的实现和优化