JavaScript’s Data Template and Event Delegation
在列表里动态添加几行数据,而且还要给加入的行添加某些按钮,比如google的搜索结果页这样:

这样的情况下,我们通常第一个想法就是用 document.createElement 来构建DOM节点,然后将数据写入到节点上,再给按钮绑上事件,然后再一个一个去 appendChild ,这样在节点数比较少的时候还靠谱,如果操作一多,每个节点都有大堆属性,样式,内容,甚至事件要构建和绑定,就很麻烦了。
模板
金大为在D2 4th上有个模板前端的讲座,他在博客上也有相应的文章。
第一步,我们实现一个最简单的引擎:
如下构建一段HTML片段(这里textarea是一个容器,模板可以在任何你想保存的地方保存):
<textarea id="J_Templ"> <li uid="$uid" class="user"> <img src="$avatar" title="$user的头像" /> <a href="$href">$user</a> <p>$status</p> <div class="operation"> <a class="J_Add">加好友</a> <a class="J_Block">列入黑名单</a> </div> </li> </textarea>
最基本的数据是这样子的:
var user = {
uid: "1",
user: "frank",
href: "http://yyfrankyy.com",
avatar: "http://en.gravatar.com/userimage/3248517/79c0b1355435ae5333ee8525e1c1e0fb.png",
status: "Hello, Template."
}
然后用 replace 来遍历替换:
function render(templ, data, container){
for(var item in data){
templ = templ.replace(new RegExp("\\$" + item, "g"), data[item]);
}
container.innerHTML += templ;
}
使用时,找到占位符,执行替换,再塞到容器里:
var templ = document.getElementById("J_Templ").value,
container = document.getElementById("container");
//渲染
render(templ, user, container);
以上就是最简单的一个JavaScript模板了。在这里,我们只需要保证数据结构是正确的,模板结构也是我们想要的就可以了。
这个简单的模板给我们带来几个好处:
- 需求变更的时候,我们需要改动以下地方:1、在模板里加入相应的占位符。2、嗯,没了。
- 充分利用JavaScript引擎对文本数据的处理,数据对象只需要查找一次,对模板执行多次遍历,数据存在时就替换,不存在时保留占位符,逻辑实在太简单了。
如果再智能一点,我们可以做一些其他工作,让这个模板支持在JSON对象里查找节点。比方说:我们完全可以这样把 $users.frank.name 变成 users["frank"]["name"],为了提高匹配度,我们用大括号 {,} 把占位符圈起来,接下来的占位符会是这样子:${users.frank.name}
新的模板引擎
var TemplateBuilder = function(tmpl, data) {
var obj = [], matchs, i = 0, output = tmpl;
//将所有匹配选项先存入数组
while (matchs = /\\$\\{([^}]*?)\\}/g.exec(output))
obj[i++] = matchs[1];
var l = obj.length;
while (l--) {
var tree = obj[l],
arr = tree.split("."),
arrLength = arr.length;
target = data[arr[0]],
i = 1;
if (target) {
while (i++ < arrLength){
//如果下级匹配不到,即认为到达JSON树的终点
if (!target[arr[i]]) break;
target = target[arr[i]];
}
//TODO: 这里还可以加入类型检查
output = output.replace(new RegExp("#\\{" + tree + "\\}", "g"), target);
}
}
return output;
}
到这里,我们可以把这样的新数据替换到新模板里去了。
新模板:
<textarea id="J_Templ">
<li uid="${users.frank.uid}" class="user">
<img src="${users.frank.avatar}" title="${users.frank.user}的头像" />
<a href="${users.frank.href}">${users.frank.user}</a>
<p>${users.frank.status}</p>
<div class="operation">
<a class="J_Add">加好友</a>
<a class="J_Block">列入黑名单</a>
</div>
</li>
</textarea>
新数据:
var users = {
frank: {
uid: "1",
user: "frank",
href: "http://yyfrankyy.com",
avatar: "http://en.gravatar.com/userimage/3248517/79c0b1355435ae5333ee8525e1c1e0fb.png",
status: "Hello, Template."
}
}
- 如果还想在复杂一点,我们定义一些变量,让模板更加可重用一点;
- 或者最起码加一个
if判断,有些东西这一行我不想让他出现了; - 还可以考虑加入循环,给模板再定义一个占位符,找到这个循环占位符的时候,里面的子模板拿出来重复
replace几次,然后一次性替换掉整块的循环占位符; - 嗯,我想把这个模板变成脚本库,那把前面这个统一的前缀
$也改造一下……
STOP!!记住,我们只是想做一个简单的可根据我自己定义的规则做一下相应替换的模板。他最初的目的,是为了减少业务变更给数据展现带来的复杂性。这样的场合非常适用于已经被抽象出来的模块层,比如操作列表,比如Gadgets。其他东西,在用数据对象同步替换数据模板的基础上,爱怎么加怎么加,根据具体的业务需求去定制。
事件代理/委托(Event Delegation)
这里还要再讨论的一个话题,在动态添加的模板文本,变成DOM结构之后,有一些按钮要有一些相应的事件。
如果你只想做业务逻辑,而不是在抽历程一个框架或者模块的时候,我的直接建议是内联。
<a onclick="alert('hello?')">SayHello</a>
因为内联是管理成本最低的,他同时也带来相应的诸多后期维护的隐患,解决这些隐患就是完全不顾这些内联代码,一有改动就修改模板,毕竟模板已经是抽象好用于不断重用的最小单元, 他的维护成本是可以预估的。
现在,我们还没开始写代码,就开始考虑:嗯,这个代码以后可能会有谁谁谁来改,或者需求变更来再次加入另外一些东西,那么我们的事件应该是用 addEventListener/attachEvent 来绑定,他的“可维护性”应该是好的,他或者是可以配置的,或者直接就用一些框架封装好的事件绑定,而当你正好用上这些函数的时候,单独用发现不行,我必须得再去调用框架的其他库,因为他是内部依赖的……
问题是,我最开始只想给这个按钮加一个事件,这个事件就是发个请求,告诉服务器删掉这行数据,数据在删除之后,删掉这行元素,同步一下视图状态就可以了。
有些情况下,我们真想把这些模板单独就构建起来一个可重用的模块,因为实际的需求里,实在太多此类情况,那么可以考虑使用事件代理/委托了:
引用NCZ的这段代码:
document.onclick = function(event){
//IE doesn't pass in the event object
event = event || window.event;
//IE uses srcElement as the target
var target = event.target || event.srcElement;
switch(target.id){
case "help-btn":
openHelp();
break;
case "save-btn":
saveDocument();
break;
case "undo-btn":
undoChanges();
break;
//others?
}
};
是的,感谢 event.target 这个东西,事件不冒泡了的情况下,我们可以很容易找到当前操作的对象,而同样不必关心这个对象是个虾米东东,而执行对这行数据所需要的参数,直接在最初的数据对象里传入就安了~
这里,我们做一个妥协,把模板转成DOM对象,因为只有DOM对象才能 addEventListener/attachEvent ……两种处理:
- 要么,自己把模板进行查找,把最外层的东西处理成DOM,这样做好像有点那个……
- 要么,在模板外再加一个容器,在这个容器里
addEventListener/attachEvent
End
至此,我们的两个话题可以融在一起,事件代理/委托只是给模板提供了绑定事件的方式。整个思路,是把核心任务放在处理数据本身,只关心数据和操作数据的事件。
数据模板充分减少了我对从HTML文本到DOM节点树的转换过程,将构建DOM树的过程交给浏览器自己去搞,我们所要做的,只是在HTML里呈现数据,加一些交互事件,其他跟DOM结构相关的东西,比如样式,比如语义,或者加入一些自定义的属性,可以较纯粹地分离开。