基本介绍
现代化的网页里面有许多原生事件,他们普遍被框架包装甚至再实现事件
,原生事件使用方法有数种:
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
这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,计算量极大,特别需求可以通过抖动解决- 原则上最好就近委托,避免阻止冒泡等逻辑影响