最近项目中遇到一个实际问题,自己搞了半天没解决,最后在StackOverflow上求助,不到3分钟就得到了解决方案,而且答案意外的很简单——加一个参数。
场景是这样的:有一个表格(事实上这样的表格在这个系统很多),每一行(row)是顾客的信息(名字,订单号,付款金额...),要求点击表格行(tr元素)打开订单详情,点击顾客名字(包裹在一个a元素里,a在td里)时打开顾客信息页面,不打开订单详情。这不简单直接么,jQuery把a元素和tr元素的事件都委托给tbody或者table不就可以了么?
$('tbody') .on('click', 'tr', function() { alert('order detail...') }) .on('click', 'a', function(e) { e.stopPropagation() alert('contact detail') })
如果只是在当前单个页面的代码里写这段,没问题。但因为项目很多页面都有这样包含"<a>顾客名字</a>"的地方,于是我干脆在一个每个页面都会引用的js文件中加了一段把这样的a元素(元素类名取为“gotocontact”)的handler委托给document元素的代码,如下(这里假设a元素是叶子元素,即不会有子元素):
// 因为此时还没引入jQuery,所以用了原生方法 document.addEventListener('click', function(e) { var el = e.srcElement || e.target; if (el && el.classList && el.classList.contains('gotocontact')) { e.stopPropagation(); alert('going to contact...') } })
然后在那个页面的代码里把tr元素的handler委托给tbody(id为“list”):
$('#list') .on('click', 'tr', function() { alert('order detail...') })
结果这样是有问题的,当点击a元素时两个handler都会触发,不符合预期。
stackoverflow上的答案是在addEventListener参数里添加第三个参数:
document.addEventListener('click', function(e) { var el = e.srcElement || e.target; if (el && el.classList && el.classList.contains('gotocontact')) { e.stopPropagation(); alert('going to contact...') } }, true) // ^^^^ add third parameter
问题解决。一个参数就解决了?!
知道addEventListener有这个参数选项(MDN),但是根本不知道起什么作用。
然后开始搜索,看到了这篇文章,算是讲得比较通俗易懂的:
W3C标准中DOM事件有3个阶段:capture phase(捕获阶段), target phase(目标阶段), bubble phase(冒泡阶段)。而常用的on<event> 和 addEventListener(event, handler) (注意没有第三个参数)只会作用于后两个阶段。如下图(来自W3C):
把W3C对这张图的说明简单翻译下搬过来:
事件生成后,事件的传播路径(propagation path)先被确定下来,路径是一个有序列表,列表中最后一个元素是目标元素,往前依次是目标元素的父元素、祖先元素,一直到Window对象。接下来事件开始传播(propagate):
1.捕获阶段。事件对象从Window对象开始沿传播路径向下,依次经过各元素传播至目标元素的父元素;
2.目标阶段。事件对象到达目标元素;
3.冒泡阶段。事件对象从目标元素的父元素开始沿传播路径向上,依次经过各元素传播至Window对象。
照着图拿本例来看。没加第三个参数时,两个handler都只在冒泡阶段起作用(jQuery的on方法就是addEventListener不加第三个参数的包装)。当我点击a.gotocontact时,事件对象从Window到达a标签元素这段时间内都没有触发handler。事件对象到达tbody#list时,触发其handler,alert('order detail...'),而此处没有stopPropagation(也不能有,否则事件传播不到document,document的handler就不会触发)。之后到达document,触发handler,alert('going to contact'),而此处的stopPropagation也只是不让事件传到最后的Window对象,事件基本走完了传播路径的一个往返。
加上第三个参数时,addEventListener添加的handler会在捕获阶段起作用。事件传播到document元素时触发handler,这里的stopPropagation停止事件进一步传播。事件刚走了一步就被停止传播,到不了tbody#list,达到预期效果。
参考:
2. UI Events
3. Event | MDN