前端原生 JavaScript 事件机制
基本介绍
现代化的网页里面有许多原生事件,他们普遍被框架包装甚至再实现事件,原生事件使用方法有数种:
GlobalEventHandlers
通过GlobalEventHandlers接口来绑定事件,例如onclick,onchange等,实现此接口的有HTMLElement,Document,Window和WorkerGlobalScope
1 | var btn = document.querySelector('button') |
行内绑定
通过行内直接绑定事件
1 | <button onclick="foo()">bar</button> |
EventTarget
通过EventTarget接口绑定事件,最常用实现接口的对象有Element,Document,Window,但是很有其他对象有实现,比如XMLHttpRequest,AudioNode,AudioContext等等
1 | var btn = document.querySelector('button') |
小结
由于EventTarget可以给单一事件绑定多个Listener,实现对象也不仅仅HTMLElement,手动触发事件和自定义事件,还有现在没有提到的更为精细的控制捕获还是冒泡触发,所以一般都是选择EventTarget的方式。
事件触发过程
1 | function foo() { |
一开始接触事件可能认为是类似上述代码,用bar来模拟事件触发,然后执行,但是实际上复杂得多,JS 事件有一个概念是捕获与冒泡,这种传递模型在Android上也有类似的实现。
但不是所有事件类型都有冒泡
1 | <body> |
捕获和冒泡是怎样运作的呢?用上面的代码来举个例子
捕获
假如click事件发生了之后,浏览器会从最外层元素去检查有没有注册捕获阶段事件,如果有,执行他,然后再到内一层元素不断循环直至已经没有子元素 ,什么意思呢,就是这边
我们所能看到的捕获流程是 body > div > button,但是实际上不仅仅如此,body外层还有html和document,所以其实最终的捕获流程是 document > html > body > div > button
冒泡
冒泡其实就是反其道而行之,当到达已经最核心元素,开始冒泡,从内到外相反顺序去检查是否注册了冒泡阶段事件,如果有执行他,然后继续。
不同阶段事件
刚刚提到有捕获阶段和冒泡阶段事件,那么这两种事件到底是怎么定义呢?其实之前所说的三种注册方法,全都是冒泡阶段事件,只有EventTarget.addEventListener(type, listener[, useCapture]) useCapture为 true 才可以注册捕获阶段事件
事件方法
stopPropagation

很多时候我们会有一些类似这样的需求,在一个可点击的 item 里面又有不同的操作,可能是点赞分享等,但是如果不阻止冒泡,很有可能就点赞的时候就点进去了 item detail 页面,这个时候就需要用到阻止传递,方法是event.stopPropagation,看了许多网上文章都说是阻止冒泡,实际上就是字面意思阻止传递,意思就是如果父元素注册了捕获阶段的方法,再调用event.stopPropagation,子元素也不会接收到任何捕获和冒泡。
preventDefault
1 | <form> |
或许也有也有用到form的时候,不希望点击马上submit,可以使用event.preventDefault来阻止默认行为,即是不会提交,同理这个也可以应用非常多地方,比如<a>,比如平常的键盘输入,全都是冒泡结束之后才会执行的。
这个方法主要判断是event.cancelable,如果自己实现自定义事件,一些情况也要考虑,当分发事件返回的是 false,就是被阻止了。
但是阻止默认行为,仅仅是不会提交或者不会跳转,而不是阻止了传递。
return false
还有一个更方便的方法可以阻止传递和阻止默认行为,就是直接在方法里面return false,它内部实际上做了三件事:
- event.preventDefault();
- event.stopPropagation();
- 停止回调函数执行并立即返回。
stopImmediatePropagation
1 | var btn = document.querySelector('button') |
event.stopImmediatePropagation用的比较少,用途就是阻止事件传递并且阻止相同事件的Listener被调用。
一开始介绍EventTarget接口的时候介绍到,同一事件可以绑定多个队列模式先进先出,如果遇到复杂需求可能用的比较多。
事件委托
1 | <ul> |
了解上述内容之后就可以了解一下事件委托,事件委托不直接把冒泡阶段事件绑定到元素上,而是绑定到他的父元素或者更上层爷爷什么的,因为是冒泡阶段事件,所以会event最终会找到点击的元素,然后再到上层元素触发,上层元素再指派给对应子元素。有点绕,但是那么折腾为啥呢?
减少内存消耗
我们经常会遇到很多列表需求,如果一个一个绑定,而这个列表长度未知,就有点浪费内存了。
动态绑定
也是这个列表来举例子,比如有需求是动态增加减少 item 数量,每次都需要手动addEventListener和removeEventListener就很麻烦了,可以减少重复工作
jQuery 事件委托
浏览器原生并没有实现事件委托,这里用 jQuery介绍:
on( events [, selector ][, data ], handler(eventObject) )
1 | $('.parent').on('click', 'child', function() { |
绑定到.parent然后触发的时候找到child
delegate( selector, eventType, handler(eventObject) )
1 | $('.parent').delegate('child', 'click', function() { |
区别不大,值得一提是使用undelegate()方法来移除。
弃用 > 3.0 版本弃用的 API
live( events, handler(eventObject) )
1 | $('child').live('click', function() { |
他可以绑定将来和现在的元素,就是依靠selector实现,如需移除unbind('child')会移除所有通过live()绑定的事件
弃用 > 1.7 版本弃用的 API | 已删除的函数
实现一个事件委托
首先肯定是把事件绑定在上层,然后通过event来判断子元素:
1 | // 给上层层元素绑定事件 |
这样就简单的实现了一个事件委托,但是一般情况还是不建议重复造轮子
局限性
- 事件委托基于事件冒泡,不适用所有事件,比如
focus,blur等就没办法了 mousemove,mouseout这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,计算量极大,特别需求可以通过抖动解决- 原则上最好就近委托,避免阻止冒泡等逻辑影响