前言
- 一次在前端技术沙龙会议上,演讲者分享主题《Node.js性能调优实践》时,提到了在node配合artTemplate渲染页面时,并发高且返回数据量大的情景下,会遇到渲染的性能瓶颈,当时演讲者有提到几种解决方法:说artTemplate在渲染模板时会使用到拷贝继承,这个效率明显低于原型继承,修改源码即可
今天去看了下最新的artTemplate源码,发现继承方法已经改为原型继承了
- 台下有人发言说也可以使用一些诸如webpack的loader加载器,将渲染的模板进行加载并预编译为实际的方法缓存起来,这样渲染的时候直接调用方法,能极大的提高效率
- 由于当时对模板引擎的原理不甚了解,听得有点蒙,事后得空来稍稍研究下
知识点普及
html嵌入模板的几种方法
- 不同方法的优劣势请参考HTML5 标签元素简介
本文选用的是方法1
<!-- 方法1 --> <script type="text/template"> <img src="xxx.png"> </script> <!-- 方法2 --> <textarea style="display: none;"> <img src="xxx.png"> </textarea> <!-- 方法3 --> <xmp style="display: none;"> <img src="xxx.png"> </xmp> <!-- 方法4 --> <template> <img src="xxx.png"> </template>
正则表达式负向前瞻
- 后文需要匹配字符串{{和{{=,这时候需要用到负向前瞻
eval和new Function的区别
- 把字符串解析为可执行的方法,据说2者运行时性能有差异,暂不讨论
浅析js模板引擎
- 不同模板间的格式以及效率对比
思路
- 在html中嵌入混有着各种原生js的模板
- 使用js读取这段模板为字符串
- 将这段字符串中特殊标记处(指{{、{{=、}})进行相应的替换,最终转换为一段可执行的代码
- 这段代码执行后将返回拼接的dom结构,之后将dom结构添加到页面
实现
待渲染的数据
let data = { name: 'hvb', age: 22, hobbies: ['reading', 'coding'], friends: [ { name: 'hwj', age: 12 }, { name: 'hwb', age: 24 } ] }
待解析的模板(规定it为数据的来源,模板里面混入了es6语法)
<script id="template" type="text/template"> <p>姓名:{{= it.name }}</p> <p>年龄:{{= it.age }}</p> <p>爱好:{{= it.hobbies[0] }}和{{= it.hobbies[1] }}</p> {{ for(let {name, age} of it.friends) { }} <p>姓名:{{= name }}</p> <p>年龄:{{= age }}</p> {{ } }} </script>
解析函数(经过大量的拼接试错,得出能使被替换后的字符串正确运行的匹配替换规则,可自行研究)
// 解析函数(注意模板中的数据来源均为it) function render(id, it) { // 获取模板字符串 let html = $(`#${id}`).html() // 把换行符替换为空 把{{替换为xx 把{{=替换为xx 把}}替换为xx html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'') // 进行简单的拼接,通过eval执行 return eval(`let str='';str+='${html}'`) }
高性能的解析函数(由于以上使用eval进行粗暴的解析,没有任何的优化,故运行效率低下,此处进行改进)
// 使用单例模式来缓存解析后的方法(对比以上粗暴的eval解析,性能提升3倍左右) let render = (function () { let fn = null return function (id, it) { if (!fn) { let html = $(`#${id}`).html() html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'') fn = new Function('it', `let str='';str+='${html}';return str;`) } return fn(it) } })()
渲染结果
// 调用方法 $('#a').append(render('template', data))
思考
- 模板引擎的思路大同小异,特别是在中途发现自己的思路跟doT.js很相近
- 至于模板引擎的其他功能,如自定义模板标签、过滤xss、include等等暂不考虑
- 模板引擎最核心是渲染性能,可以参考高性能JavaScript模板引擎原理解析
源码
以下还小小的对比了下自定义模板和doT.js的渲染性能,当然这对doT.js并不公平,因为我只实现了很小的一个功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<title>demo</title>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4="
crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/dot/2.0.0-beta.0/doT.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
body,
html {
width: 100%;
height: 100%;
}
div {
border: 2px solid red;
}
</style>
</head>
<body>
<h2>模板解析如下:</h2>
<div id="a"></div>
<h2>模板解析如下:(使用不同数据)</h2>
<div id="b"></div>
<script id="template" type="text/template">
<p>姓名:{{= it.name }}</p>
<p>年龄:{{= it.age }}</p>
<p>爱好:{{= it.hobbies[0] }}和{{= it.hobbies[1] }}</p>
{{ for(let {name, age} of it.friends) { }}
<p>姓名:{{= name }}</p>
<p>年龄:{{= age }}</p>
{{ } }}
</script>
</body>
<script>
$(() => {
// 一组数据
let data1 = {
name: 'hvb',
age: 22,
hobbies: ['reading', 'coding'],
friends: [
{ name: 'hwj', age: 12 },
{ name: 'hwb', age: 24 }
]
}
// 一组不同的数据
let data2 = {
name: 'hvb111',
age: 22111,
hobbies: ['reading111', 'coding111'],
friends: [
{ name: 'hwj111', age: 12111 },
{ name: 'hwb111', age: 24111 }
]
}
// 使用单例模式来缓存解析后的方法(对比以上粗暴的eval解析,性能提升3倍左右)
let render = (function () {
let fn = null
return function (id, it) {
if (!fn) {
let html = $(`#${id}`).html()
html = html.replace(/\n/g, '').replace(/{{(?!=)/g, '\';').replace(/{{=/g, '\'+').replace(/}}/g, ';str+=\'')
fn = new Function('it', `let str='';str+='${html}';return str;`)
}
return fn(it)
}
})()
// 使用模板渲染一组数据
$('#a').append(render('template', data1))
// 使用模板渲染一组不同的数据
$('#b').append(render('template', data2))
// 自定义模板性能测试
console.time()
for (let i = 0; i < 1000; i++) {
$('body').append(render('template', data1))
}
console.timeEnd()
// doT模板性能测试
var evalText = doT.template($('#template').text())
console.time()
for (let i = 0; i < 1000; i++) {
$('body').append(evalText(data1))
}
console.timeEnd()
})
</script>
</html>