接手一个项目,前端用codemirror来展示了一段代码,我想添加一个鼠标点击某一行的事件,却发现怎么都不会调用我绑定的关于click事件的函数。

html事件触发的机制###

与其抓破脑袋想为什么没有触发绑定的事件函数,不妨先想想有哪些方式可以让绑定的事件函数失效。

首先,根据W3C标准,DOM事件触发分为了三个阶段:

The standard DOM Events describes 3 phases of event propagation:

  1. Capturing phase – the event goes down to the element.
  2. Target phase – the event reached the target element.
  3. Bubbling phase – the event bubbles up from the element.

Here’s the picture of a click on <td> inside a table, taken from the specification:

img

讲得更具体一些,以冒泡阶段(bubbling phase)为例,当一个DOM元素触发了一个事件时,首先,该元素对应该事件的处理函数会被调用(如果有的话),然后,该事件会继续传播(或者称之为冒泡(bubbles up))到该元素外层的DOM元素中,并触发其对应的处理函数,以此类推。

因此,大胆推测,我们可以有这些方式来阻止事件函数的触发:

  • 在事件“传递”路径上进行劫持,即在上述某个阶段阻止事件进一步传递到目标元素,但副作用是“传递”过程被截断了,目标元素之后的元素也无法获知该事件的存在了。
  • 在目标元素本身的事件触发之前进行劫持,不清楚是否存在这样的接口,可以在目标元素事件真正触发之前进行操作,并且不影响事件的继续“传递”。
  • 取消绑定目标元素的事件函数,但取消绑定之后是不是还需要再在适当的时候绑定回来?这种方式感觉比较蠢。

在事件“传递”路径上进行劫持

首先大部分情况下Capturing阶段对我们是隐藏,但可以通过设置addEventListener的第三个参数为true来实现Capturing阶段事件的绑定:

Handlers added using on<event>-property or using HTML attributes or using addEventListener(event, handler) don’t know anything about capturing, they only run on the 2nd and 3rd phases.

To catch an event on the capturing phase, we need to set the 3rd argument of addEventListener to true.

所以这里只讲下Bubbling阶段如何截获事件触发,Capturing阶段同理。

在事件函数中,可以通过event.stopPropagation()来阻止事件进一步传播,举个例子,我们有如下html:

<div id="outer">
    <div id="inner">
    </div>
</div>

可以通过下面的方式来阻止父元素的对应的事件的触发:

var inner = document.getElementById("inner");    
inner.addEventListener("click", function (e) {
  e.preventDefault();  // this prevent the default handler, here the default handler does nothing at all
  e.stopPropagation();  // this prevent the event goes to listener functions of outer DOM elements
});

需要注意的是,如果是原生的JavaScript,只有上述方式可以达到这个目的,如果是JQuery的话,可以直接return false来代替上面函数里的内容,参考event.preventDefault() vs. return false

另外需要注意的是,无论是哪种方式阻止了事件的触发,从Chrome开发工具上来看,该元素的事件所绑定的函数仍然是之前我们绑定的,所以一般来说阻止仅仅是屏蔽了事件的触发,而不会对事件对应的函数绑定做手脚

Chrome开发工具上仍然能正确查看到对应的事件函数

可以看到这种方式阻止的事件传播非常隐蔽,因此必须要慎用!否则可能就是埋下大坑。

在目标元素事件触发之前进行劫持###

尚不清楚是否存在这样的API来使用JavaScript进行操作,但产生这次开头那个问题的“元凶”应该是这种方式,且它用的是CSS:

.CodeMirror {
    /* Disable clicks so that no inconsistent state can be reached.
       `active-line` is only changed via the state viewer's handlers. */
    pointer-events: none;
}

取消事件函数的绑定###

使用event.removeEventListener可以达到目的。

一些感想###

前端中CSS和JavaScript有些功能重叠了,其实有时候是挺恶心的,出了问题你都不知道是JavaScript代码的问题还是CSS代码的问题。这类问题,我觉得要么靠丰富的经验可以快速定位,要么就得靠代码规范来规避了。而且某些经验其实会越来越稀缺,毕竟各类库和标准都是在不断翻新变化的,尤其是前端领域。从产品的角度来看,不要把宝压在某些人身上,通过一些规范(不仅仅是代码层面的规范)让任何人都能快速上手并参与到产品开发维护中才是正确的打开方式。

另外,感觉前端的门槛还是不低的,至少要把W3C标准弄清楚才能规避一些坑,并且不仅需要熟悉原生的JavaScript,还要了解各种框架,记各种API,甚至要熟悉对应的后端开发。

Reference###

  1. Bubbling and capturing
  2. event.preventDefault() vs. return false
  3. W3C