JavaScript事件委托比你想像的简单

本文略译自《JavaScript Event Delegation is Easier than You Think》。

如果你有过一点前端JS开发的经验,一定听说过事件委托(event delegation)或事件代理。这个概念听起来很高级,事实上,如果你已经熟悉如何给页面元素添加事件处理程序(event handlers),你几乎已经知道如何编写事件委托。

JavaScript事件模型」是网页交互功能的技术基础,一般情况下,你需要一项交互功能(例如点击交互),就为交互元素安装一个事件处理程序。然而,事件处理程序的安装是会引起内存泄漏和性能下降的,你安装得越多,性能风险就越大。事件委托的目的是要「降低事件处理安装数量」,它原理就是,通过给DOM树的父元素安装单一个事件处理程序,来避免给多个子元素安装事件处理程序

事件委托是什么原理?

事件委托这种“高级技术”基于「JavaScript事件模型」两个不太起眼的功能:事件冒泡(event bubbling )和事件目标(target element)。当页面上某一个元素触发一个事件,例如鼠标点击了一个按钮,元素的所有父元素也会触发该事件,这就是事件冒泡——事件从原初元素开始向上“冒泡”直到DOM树的顶。

JavaScript事件模型」有事件捕获阶段的概念,分别是潜水(一般译捕获)或冒泡两人个阶段;

事实上,事件捕获阶段(潜水或冒泡)现象只有「DOM对象事件」有,因为DOM对象存在一种视觉上或结构上的层叠关系;而像一些独立对象(window, XMLHttpRequest)没有事件捕获阶段的概念。

在「JavaScript事件模型」中,任何交互事件都有一个事件对象(event object)表征计算,触发事件的原初元素/对象由「事件对象的target属性」引用。事件对象是由浏览器提供的,算是「JavaScript事件模型」重要API对象。

事件委托就是利用「事件冒泡」收集多个子元素的事件,然后利用「事件目标」判断谁触发了事件,再执行特定事件处理逻辑。任何一个对象都可以触发事件,但不一定安装有处理程序,有了事件冒泡,子元素的事件的可以由父元素的处理程序代为处理。

有例子吗?

假如我们的页面有一张10列100行的表格(HTML table),我们要实现,当用户点中表格中任何一个单元,都会有一次单单击的交互。例如,用户点中表格单元,单元呈现出一种可编辑状态,提示可改变大小。如果「为1000个表格单元分别安装事件处理程序」,那将会产生严重的性能问题。相反,使用事件委托,你只需给表格安装一事件处理程序,在程序中再判断哪一个单元格被单击。

能再具体点吗?

具体代码也是比较简单的。编码的重要思考点是,在唯一的「事件委托程序里」判断事件目标(target element)。假如我们有一个表格,它的ID是“report” ,然后我们要为它安装一个点击事件的处理程序,叫editCell。那在editCell里我们需要检测事件目标是谁。

所有「事件处理程序」都是由浏览器代替用户进行触发执行的,并且调用时统一传递一个「事件对象」,表征事件信息(这是「JavaScript事件模型」最基本的规则),我们可在这个「事件对象」上检测到「事件目标」,考虑到检测「事件目标」的功能非常通用,所以可独立写一个功能函数getEventTarget

function getEventTarget(e) { 
    e = e || window.event; 
    return e.target || e.srcElement; 
}

函数的功能非常简单,直接读取并返回传入的「事件对象」的「事件目标」属性。由于IE与其它的浏览器的事件模型不兼容——事件对象和事件对象属性名不同,所以可能要考虑加入跨浏览器(cross-browser)的判断处理。

在表格的click事件委托处理程序editCell 中,我们调用取得事件目标引用,再根据你的需求进一步做具体的事件处理,例如判断是不是td元素,是哪一个td元素,再做逻辑处理:

function editCell(e) {
  var target = getEventTarget(e);
  if(target.tagName.toLowerCase() === 'td') {
    // DO SOMETHING WITH THE CELL
  }
}

上面的例子里,我们通过检测事件目标的tag来判断它是不是一个表格单元。这里的检查是简单化的,实践中「判断事件目标的任务」可能比较复杂。例如,如果表格单元里边还有元素,事件是由这内嵌元素触发的,怎么判断是哪个表格单元?这种情况,我们需要额外的代码来决断到底是哪一个表格单元触发了事件;又,如果有一些表格单元不可编辑呢?在这种情况下,我们需要用一个CSS类来区分不可编辑单元,代码需要判断只有可编辑单元才将它转为可编辑状态。同一个「判断任务」会多种可选的方案,你只需按照的具体需求来选择就可以了。

事件委托的益处和使用注意

二大益处

使用事件委托统一管理多个子元素的事件处理,有两个益处:

第一,最大的益处,就是减少了事件处理程序的安装数量,降低了内存开销,提高了性能,同时降低了程序崩溃风险;

第二,当DOM更改时不用重新安装事件处理程序,这主要指子元素。如果你的页面内容动态生成,例如通过ajax技术,那么你无需再手动为这些动态元素安装或卸载事件处理程序。

三个注意

事件委托使用的潜在问题比较隐晦,不过只要你注意下,还是比较容易避免:

第一,事件委托将多个事件的代码集中在一起,加高了造成性能瓶颈(performance bottleneck)的风险,所以,尽量让事件委托的代码轻盈;(EM:何以证明性能瓶颈?)

第二,上面已经提到,并不是所有的事件类型都会有“冒泡”现象;blur, focus, load and unload 不会“冒泡”。blur和 focus事件倒是可在事件潜水阶段被访问到(非IE浏览器),但这里不宜讨论;

第三,你需特别注意为「鼠标事件」编写事件委托,例如如果你要为鼠标移动的mousemove编写事件处理,你的代码极有可产生性能瓶颈,因为mousemove事件的触发频率很高。另外,鼠标移出的事件很诡异,很难在事件委托里进行管理。

小结

各大流行JS库都认为事件处理(包括事件委托)杂音(verb)太多,都提供高级API,包括jQuery, Prototype, and Yahoo! UI,它们都有相应的事件委托的例子。

裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.