一般通过如下的方法对元素添加监听事件
target.addEventListener(type, listener[, options]);
target.addEventListener(type, listener[, useCapture]);
当第3个参数是object时,可以有下面的参数
- capture
Boolean
值,表明监听函数在捕获阶段触发 - once
Boolean
值,表明监听函数在触发一次之后,就会自动从监听事件的列表中移除(相当于removeEventListener) - passive
Boolean
值,表明在监听函数内部不会使用preventDefault()
来取消浏览器的默认行为。即使调用preventDefault()
也没有任何效果
关于 passive
的介绍可以看 这篇文章
执行顺序
首先,现在的浏览器基本都支持 冒泡+捕获 的事件流模型,这里就不在多加介绍
执行顺序按照下面的规则(个人摸索出来的规则)
对事件对象来说: window
> document
> body
> 父类元素 > 事件目标
对注册类型来说: 内联事件 优先于 通过代码注册的事件(内联事件也就是写在html元素标签属性上的事件,如 <a οnclick='...'></a>)
对事件触发阶段来说: 捕获 优先于 冒泡
对注册事件的顺序来说: 先注册的先调用
通过实际例子检验一下:
<div id='div1'>
<div id='div2'>
<div id='div3'>
<button type='button' onclick='console.log("inline onclick")'>Test</button>
<p>11111111111111</p>
</div>
</div>
</div>
function x(e){
//用 eventPhase 来检测当前事件的处理阶段
return e.eventPhase==3 ? 'bubbles':'capture '
}
window.addEventListener('click',(e)=>{
console.log('window click 0 in '+ x(e),e)
});
window.addEventListener('click',(e)=>{
console.log('window click 1 in '+ x(e),e)
});
window.addEventListener('click',(e)=>{
console.log('window click 2 in '+ x(e),e)
},true);
document.addEventListener('click',(e)=>{
console.log('document click in ' + x(e) ,e)
})
document.addEventListener('click',(e)=>{
console.log('document click in ' + x(e) ,e)
},true)
document.querySelector('#div2').addEventListener('click',(e)=>{
console.log('----start------')
console.log('event in ' + x(e),e)
console.log('target',e.target)
console.log('current target',e.currentTarget)
console.log('----end------')
})
document.querySelector('button').addEventListener('click',(e)=>{
console.log('----start------')
console.log('event in ' + x(e),e)
console.log('target',e.target)
console.log('current target',e.currentTarget)
console.log('----end------')
},true)
点击按钮,我们可以看一下控制台是如何输出的
一般是使用下方两个方法中断事件流的传播(注意,阻止事件传播不是销毁event对象,event依然会返回给浏览器)
event.stopPropagation() // 阻止event往其他事件目标传播,但是当前事件目标其他的监听函数会依次调用
event.stopImmediatePropagation() // 立即阻止event传播,不会调用其他的监听函数
阻止事件执行浏览器的默认行为(调用之后无法恢复)
event.preventDefault()
有些事件的触发机制是连续性的,比如 click
事件需要 mousedown
和 mouseup
这两个前置事件的触发(鼠标按键按下然后弹起才算是一个完整的点击),如果在mouseup
里用event.preventDefault()
阻止mouseup的默认行为,click
事件也因此无法触发。 类似的有 keypress
需要 keydown
和keyup
,还有其他。
这里有一个简单的应用:比如一个网站使用这样的代码document.addEventListener('copy',(e)=>{e.preventDefault()})
让用户不能复制页面上的内容。 要突破复制限制,一般的解决方法是用 removeEventListener
,可是这里行不通,因为监听函数是一个匿名函数,而你又无法得到这个函数的引用。 替代的方案是window.addEventListener('copy',(e)=>{e.stopImmediatePropagation()})
这里利用了监听函数执行的优先顺序,提前终止了事件的传播,让网站阻止复制的代码无法执行。
自定义事件
Event
接口代表了DOM中所有事件,是所有事件的父类。在DOM中事件的种类非常多(链接),他们的初始化参数也比较复杂,不可能一一列举,这里只介绍几种用代码的方式自动义事件的例子(不推荐使用 document.createEvent的方式构建事件)
Event 构造函数 (初始化参数请参考文档)
event = new Event(typeArg, eventInit);
typeArg
表示这个事件的名称,可以按照自己的需求定义。如果名称定义为浏览器内置事件的名称,这个事件是没有实际效果的,因为这些事件都有属于他们自己的构造函数,如 click
对应 new MouseEvent
。
eventInit
对象有3个参数
- bubbles
Boolean
值,表明事件是否需要冒泡阶段 - cancelable
Boolean
值,表明事件是否可以取消 - composed
Boolean
值,跟 Shadow DOM 有关,暂时不了解作用
eventInit
只有3个参数,基本没什么用处,也不能传递数据,所以通过Event
构造函数创建的事件只有简单的通知作用,告诉你有某个事件被触发了,然后就没了。
MouseEvent 构造函数(初始化参数请参考文档)
event = new MouseEvent(typeArg, mouseEventInit);
mouseEventInit 可用的参数比较多,选择几个重要的说明一下
mouseEventInit = {
button: 0, // 鼠标事件的按钮,0是左键,1是中键,2是右键
ctrlKey: false,
shiftKey: false,
altKey: false, // 对应于键盘上的三个控制键是否被按下
screenX: 0,
screenY: 0, // 对应事件触发时鼠标在用户屏幕上的坐标
clientX: 0,
clientY:0, // 对应事件触发时鼠标在用户浏览器界面上的坐标
}
创建一个鼠标事件可以这样
var event = new MouseEvent('click',{}) // 对,什么都不要,因为参数都有默认值
或者正规一点
var event = new MouseEvent('click', {
button: 1,
view: window,
bubbles: true,
cancelable: true
});
CustomEvent 构造函数
event = new CustomEvent(typeArg, customEventInit);
CustomEvent顾名思义,就是自定义事件,它可以让我们把数据放在事件对象里面,并且随着事件的传播而传播(终于有一个实用的了),而上面两个事件一般都由浏览器根据用户的行为触发
var event = new CustomEvent('cat', {
detail: { // 数据放在 detail 下面,支持任何类型的数据,比如这里的数据类型是一个对象
data: 'balabalalalala',
getMySalary: function(){
return 20000;
}
}
});
要创建浏览器自带事件,并用代码来模拟事件的触发,需要选择正确构造函数和正确的构造参数来创建事件。这些参数都在 MDN 上有详细的介绍,想对事件深入理解的话不妨认真阅读。
事件触发
我个人了解到的总共有这3种触发方式
- 用户事件触发,浏览器自己触发事件
- 因为事件的传播而被动触发,比如事件委托
- 用 element.dispatchEvent(event) 或 用 click()、focus()、blur()等 代码的方式主动触发
这三种方法的触发是相互影响的,并不能完全独立开来。
“原生”事件如浏览器自己触发事件在执行监听函数时是异步执行的,但是 element.dispatchEvent(event)
是同步执行的,也就是说,dispatchEvent调用后,当前的JS运行线程会阻塞在这里,直到所有的监听函数被执行并结束。
鉴别是浏览器派发的事件还是通过JS代码的方式手动触发的事件,只需检测 event
里的isTrused
属性,为true
就是浏览器触发的,false
则是代码触发的,这个属性无法用初始化参数进行初始化