这是看Railscasts 229 Polling For Changes时想到的一个问题,就是Rails的Unobtrusive Javascript如何去为一个不存在于DOM树中的对象去绑定事件。
举个例子:
我有一个列表,就是用Rails的scaffold生成的那种,每行显示一个产品信息,后面有三个按钮(显示,编辑,和删除),整个列表用 table 标签,每一行用 tr 标签。以下是一行的例子。为了方便我只写了Delete按钮,因为只有它才绑定了JS。
<tr class="product"> <td>iPhone</td> <td> <a rel="nofollow" data-method="delete" data-confirm="Are you sure?" href="/products/1">Destroy</a> </td> </tr>
我们知道Unobtrusive Javascript的做法,是在整个页面加载之前,搜索DOM树,然后为特定的标签去绑定Javascript函数。比如上面的html中的 a 标签,它有 data-confirm 属性,Rails的Unobtrusive Javascript会为它绑定特定的函数,让我们点击这个按钮时,弹出 “Are you sure ?” 的提示信息。
现在设想一种情况,我们用Ajax来实时更新这个列表,比如每隔几秒钟去读一下,然后把新的product增量添加到列表的最下面。这时,因为新增加的那一列是在页面加载完成之后才添加进DOM树的,按道理这时是不会为新那一列的Delete按钮绑定事件的。但实际上,Rails很好的处理了这种情况,无论是通过什么方法去修改或替换DOM节点,那些特殊节点的事件都会正确执行。
于是我想知道Rails是如何处理事件绑定的,因为我那个Railscast的例子中用的 jquery-rails 这个gem,所以我查看了它提供的rails.js文件。发现它处理 data-confirm 的。
$('a[data-confirm],input[data-confirm]').live('click', function () { var el = $(this); if (el.triggerAndReturn('confirm')) { if (!confirm(el.attr('data-confirm'))) { return false; } } });
这里它是用 live 方法来为找到的DOM节点(实际上应该叫jQuery包装集)绑定事件的。再去查查 jQuery的文档,发现它对 live 方法的描述如下:
Attach a handler to the event for all elements which match the current selector, now or in the future.
大概意思是说,live 方法可以为选择器找到的对象绑定事件处理函数,不管这个对象是已有的,还是以后要添加进来的。
关于如何做到这点,jQuery的文档中也有详细的介绍,大致就是:jQuery并不是把Javascript函数绑定到选择器查找出来的节点,而是绑定到html文档的根节点。当我们触发子节点的事件后,通过事件冒泡,来调用绑定到根节点上的Javascript函数。有兴趣的可以看看jQuery的文档说明,说得还是很详细的。
因为我原来写过一个基于Prototype的JS列表,用处和上面说的差不多。而我当时没找到Prototype有类似jQuery的live函数。所以我又看了下Prototype版本的 rails.js 文件。发现Prototype是这样处理的:
document.on("click", "*[data-confirm]", function(event, element) { var message = element.readAttribute('data-confirm'); if (!confirm(message)) event.stop(); });
Prototype是用 on 方法来处理的,原理和jQuery差不多,就不多说了。有兴趣的可以看看Prototype的文档说明(这个就太简洁了)。不过从语法可以比较明显的看到,函数是直接绑定给document节点的。相对而言,我更喜欢 jQuery 的API风格,这点就见仁见智了。