Micro-Templating性能优化

Heero.Luo发表于2年前,已被查看586次

这篇文章中,我简单介绍了前端模板引擎。John Resig写的tmpl函数麻雀虽小五脏俱全,足以满足日常开发需要。本文主要探讨一下tmpl的性能优化。

先复习一下tmpl的源代码:

var tmpl = (function() {
	var cache = { };

	return function(str, data) {
		var fn = cache[str];
		if (!fn) {
			fn = new Function("obj",
				"var p=[];" +
				"with(obj){p.push('" +
				str
					.replace(/[\r\t\n]/g, " ")
					.split("<%").join("\t")
					.replace(/((^|%>)[^\t]*)'/g, "$1\r")
					.replace(/\t=(.*?)%>/g, "',$1,'")
					.split("\t").join("');")
					.split("%>").join("p.push('")
					.split("\r").join("\\'")
				+ "');}return p.join('');"
			);
			cache[str] = fn;
		}

		return fn(data);
	};
})();

其实现原理是:模板内容(str变量)经过一系列字符串操作替换成原生的Javascript语句,再通过new Function创建为模板函数并缓存起来,把数据传入模板函数执行后,其返回值就是渲染结果。我们把从模板字符串到模板函数的转换过程称作模板编译。可见,由于缓存(cache对象)的存在,对同一份模板而言,编译只会发生一次。所以,改良模板函数的执行过程对性能提升帮助更大。为了加深理解,我们把fn打印出来看看:

var template =
'<ul>' +
'<% for (var i = 0; i < list.length; i++) { %>' +
	'<li><a href="<%=list[i].url%>" target="_blank"><%=list[i].title%></a></li>' +
'<% } %>' +
'</ul>';

var list = [
	{ url: 'http://www.baidu.com/', title: '百度' },
	{ url: 'http://www.163.com/', title: '网易' },
	{ url: 'http://www.weibo.com/', title: '微博' },
	{ url: 'http://www.youku.com/', title: '优酷' },
	{ url: 'http://www.cnbeta.com/', title: 'cnBeta' }
];

var tmpl = (function() {
	var cache = { };

	return function(str, data) {
		var fn = cache[str];
		if (!fn) {
			fn = new Function('...此处省略...');
			console.log(fn);
			cache[str] = fn;
		}

		return fn(data);
	};
})();

tmpl(template, { list: list });
// 打印结果
function anonymous(obj
/**/) {
var p=[];with(obj){p.push('<ul>'); for (var i = 0; i < list.length; i++) { p.push('<li><a href="',list[i].url,'" target="_blank">',list[i].title,'</a></li>'); } p.push('</ul>');}return p.join('');
}

这个函数主要有两个性能问题:

  • 使用数组拼接字符串。在稍微新一点的浏览器中,原生字符串拼接比数组拼接性能更高。
  • with语句是性能杀手。

第一个问题很好解决,把数组操作都改成字符串操作即可;第二个问题就比较麻烦,如果没有了with语句,如何让函数正常执行呢?

function anonymous() {
var p=[];p.push('<ul>'); for (var i = 0; i < list.length; i++) { p.push('<li><a href="',list[i].url,'" target="_blank">',list[i].title,'</a></li>'); } p.push('</ul>');return p.join('');
}

办法还是有的,只要能把list作为函数参数即可。代码如下(暂时不考虑缓存):

var tmpl_optimized = (function() {
	function compile(str) {
		return  "var __result__='';" +
			"__result__+='" +
			str
				.replace(/[\r\t\n]/g, " ")
				.split("<%").join("\t")
				.replace(/((^|%>)[^\t]*)'/g, "$1\r")
				.replace(/\t=(.*?)%>/g, "'+$1+'")
				.split("\t").join("';")
				.split("%>").join("__result__+='")
				.split("\r").join("\\'")
			+ "';return __result__";
	}

	return function(str, data) {
		var keys = [ ], values = [ ];
		for (var k in data) {
			if (data.hasOwnProperty(k)) {
				keys.push(k);
				values.push(data[k]);
			}
		}

		var fn = new Function(keys, compile(str));
		console.log(fn);
		return fn.apply(this, values);
	};
});

tmpl_optimized(template, { list: list });

这段代码的关键点:

  • 模板可能会用到data的所有属性,所以要进行遍历提取所有属性名(keys)和值(values);
  • keys数组即为模板函数的参数列表;
  • values的顺序与keys相对应,所以fn.apply(this, values)就可以依次把参数传进去。

此时打印出来的fn为:

 function anonymous(list
/**/) {
var __result__='';__result__+='<ul>'; for (var i = 0; i < list.length; i++) { __result__+='<li><a href="'+list[i].url+'" target="_blank">'+list[i].title+'</a></li>'; } __result__+='</ul>';return __result__
}

恰好达到前面说的目的。小试牛刀已经成功,下面就要研究如何加上缓存机制了。tmpl是使用模板本身作为缓存标识的,但是在tmpl_optimized中就不能只这么干了,因为对同一份模板来说,不同的数据可能产生不同的模板函数,例如:

  • 当data为 { a: 1, b: 2 } 时,模板函数为 function(a, b) { ... } ;
  • 当data为 { c: 1, d: 2 } 时,模板函数为 function(c, d) { ... } 。

也就是说,data对象内属性的差异会产生不同的模板函数,所以这里需要两层缓存,第一层用于记录compile函数的返回结果,第二层用于记录特定属性组合下的模板函数:

var tmpl_optimized = (function() {
	var cache = { };

	function compile(str) {
		return  "var __result__='';" +
			"__result__+='" +
			str
				.replace(/[\r\t\n]/g, " ")
				.split("<%").join("\t")
				.replace(/((^|%>)[^\t]*)'/g, "$1\r")
				.replace(/\t=(.*?)%>/g, "'+$1+'")
				.split("\t").join("';")
				.split("%>").join("__result__+='")
				.split("\r").join("\\'")
			+ "';return __result__";
	}

	return function(str, data) {
		// 第一层缓存,使用模板作为缓存标识
		var tplObj = cache[str];
		if (!tplObj) {
			tplObj = cache[str] = {
				fnBody: compile(str),
				fnCache: { }
			};
		}

		var keys = [ ], values = [ ];
		for (var k in data) {
			if (data.hasOwnProperty(k)) {
				keys.push(k);
				values.push(data[k]);
			}
		}

		// 第二层缓存,使用属性名作为缓存标识
		var cacheKey = keys.toString(), fn = tplObj.fnCache[cacheKey];
		if (!fn) {
			fn = tplObj.fnCache[cacheKey] = new Function(keys, tplObj.fnBody);
		}

		return fn.apply(this, values);
	};
})();

最后可以通过这段代码来对比tmpl和tmpl_optimized执行时间上的差异:

console.time('original');
for (var i = 0; i < 10; i++) {
	tmpl(template, { list: list });
}
console.timeEnd('original');

console.time('optimized');
for (var i = 0; i < 10; i++) {
	tmpl_optimized(template, { list: list });
}
console.timeEnd('optimized');

由于不同机器有不同的性能,所以这里就不详细贴数据了。此外,性能优化的手段并不是永远都有效的,可能将来有一天JS引擎对with语句进行大幅度优化后,tmpl会比tmpl_optimized更高效。

评论 (1条)

发表评论

(必填)

(选填,不公开)

(选填,不公开)

(必填)