浏览器事件
常见的浏览器事件
- 鼠标事件
click鼠标点击触发mousemove鼠标移动时触发mouseleave鼠标离开某元素时触发mouseenter/mouseout鼠标移进移出事件mousedown/mouseup按下/释放鼠标事件contextmenu鼠标右键点击事件
mouseleave & mouseout的差别 两者同是鼠标移开事件, 不同点是mouseout的触发对象不仅是目标元素还包括目标元素的子元素, 而mouseleave只在鼠标理开目标元素(把子元素当作目标元素的一部分)之后离开。
- 键盘事件
keydown/keydup键盘按下松开事件
DOM事件
copycutscorllloadresize
- 表单事件
submitreset
添加事件处理
html 行内添加
使用元素的 on<event> 属性添加事件。
<button onclick="alert(1)"></button>
或者
<button onclick="handleClick()"></button>
<script>
function handleClick() {
alert(1)
}
</script>
JS DOM 调用
<button id="btn" onclick="handleClick()"></button>
<script>
const btn = document.getElementById('btn')
btn.onclick = function() {
alert(1)
}
</script>
注意事项
this指向 事件处理程序中的this指向对应的元素
<button onclick="alert(this.innerHTML)">click button</button>
上面的代码中
this.innerHTML显示元素的内容
- 程序分配 & 程序执行
当使用 js DOM 调用时,函数应该被分配给 DOM.on<event>, 而不是执行。
// 正确
button.onclick = sayThanks;
// 错误
button.onclick = sayThanks();
当作为 html 的 on<event> 属性时,浏览器会创建一个处理程序,在这个处理程序中希望执行一些代码。
<button onclick="sayThanks()">click button</button>
on<event> 模式弊端
当需要在元素上为一个事件分配两个事件处理是不被允许的。新的旧的处理程序总是会被覆盖。
<button id="btn">click btn</button>
<script>
const btn = document.getElementById('btn')
btn.onclick = function1(){}
btn.onclick = function2(){}
</script>
以上代码只执行 function1
addEventListener 事件注册
该方法将指定的监听注册器注册到目标元素上, 当该元素触发指定的事件时,指定的回调函数就会被执行。
使用方法
ele.addEventListener(event, handler[, options])
event 事件名称
handler 处理程序
options 附加选项
once如果为true, 那么事件触发后将会自动删除监听器capture事件处理阶段, [事件冒泡/捕获]passive如果为true,preventDefault()将不会生效
<button id='btn'>btn</button>
<a href="" id='abtn'>demo</a>
<script>
let btn = document.getElementById('btn')
let abtn = document.getElementById('abtn')
abtn.addEventListener('click', function(e){
e.preventDefault()
console.log(e, this)
}, {passive: false}) // 阻止 a 标签的默认跳转行为
</script>
对于上面使用 on<event> 形式不能在一个元素上面同时绑定两个事件的问题, addEventListener 可以很好的解决。
btn.addEventListener('click', function(e){
console.log('fn1')
}, false)
btn.addEventListener('click', function(e){
console.log('fn2')
}, false)
上面代码这个事件都将会执行。
removeEventListener 移除事件注册
element.removeEventListener(event, handler[, options])
removeEventListener 函数中参数必须同 addEventlistener 中保持一致。
handler 保持一致
为了使 handler 处理程序在 addEventListener 和 removeEventListener 中保持一致, 需要把函数储存在一个变量中。
function handler() {
console.log('handler')
}
btn.addEventListener('click', handler)
btn.removeEventListener('click', handler)
如果不使用这种方式事件将不能移除 下面的代码不会起作用。
elem.addEventListener( "click" , () => alert('Thanks!'));
// ....
elem.removeEventListener( "click", () => alert('Thanks!'));
event 对象
无论使用 on<event> 还是使用 addEventListener 绑定事件,在处理函数中有都会有一个 event 事件对象参数。
btn.onclick = function(event){
console.log(event)
}
btn.addEventListener('click', function(event){
console.log(event)
})
// MosueEvent {......}
event 对象常用的属性
event.type事件类型的名称, 如clickmouseoverevent.target触发事件的元素。event.currentTarget触发事件的当前元素。event.clientX / event.clientY鼠标相对于窗口的x/y轴坐标
事件冒泡和捕获
DOM 事件执行的3个阶段:
1. 捕获阶段 自上向下
2. 执行阶段
3. 冒泡阶段 自下向上
冒泡
当一个事件发生时, 会首先运行该元素上的处理程序, 然后运行其父级元素上的处理程序,然后一直向上到其祖元素(document 或 window)上的处理程序。 是一个事件从下到上的事件穿透过程。
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
当点击 p 元素的时候会分别有3个 alert 执行,顺序为 p -> div -> form
event.target & event.currentTarget
上一节讲到 handler 函数中的 event 对象包含了 target 和 currentTarget 两个属性, 他们在冒泡过程中存在区别。
改写一下上面的例子
<form id='form'>FORM
<div id='div'>DIV
<p id="p">P</p>
</div>
</form>
<script>
const form = document.getElementById('form')
const div = document.getElementById('div')
const p = document.getElementById('p')
function handler(event) {
console.log(event.target, event.currentTarget)
}
form.addEventListener('click', handler)
div.addEventListener('click', handler)
p.addEventListener('click', handler)
</script>
当点击 p 元素时, 输出结果:
p 元素 / P 元素 -> p 元素 / div 元素 -> p 元素 / from 元素
可以看出
event.target是引发事件的目标元素, 在事件冒泡的过程中并不会发生改变。event.currentTarget表示当前正在执行事件的元素(冒泡到该元素)。
停止冒泡
事件冒泡从目标元素开始,会一直上升到顶级元素 document, 有些事件也可以到达 window, 这个过程要调用路径上所有的处理程序。
这个路径上任何一个元素都可以使用 event.stopPropagation() 停止冒泡。
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="event.stopPropagation()">P</p>
</div>
</form>
当使用
addEventListener为一个元素添加多个事件程序时候, 即使阻止了其中一个事件冒泡, 其他的处理程序依旧会依序执行造成冒泡, 使用event.stopImmediatePropagation()用于停止冒泡,并阻止当前元素上其他的处理程序运行。
捕获
事件执行需要经过三个阶段: 1.事件捕获。 2. 事件执行。 3.事件冒泡。 实际上事件在执行前都会经历过一个事件捕获阶段。这个阶段从 window 开始一直向下穿透捕获到目标元素,从而给元素执行事件程序,但是默认情况下浏览器执行事件时已经忽略了事件捕获阶段,事件仅在第二阶段和第三阶段执行。
开启事件捕获阶段
在 addEventListener 函数中,将 capture 参数设置为 true 。
btn.addEventListener('click', handler, {capture: true})
例子:
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}, ${e.eventPhase}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}, ${e.eventPhase}`));
}
</script>
代码中为 form 元素及子元素都注册了 捕获事件和一般事件,执行顺序为
HTML -> BODY -> FROM -> DIV -> P(捕获阶段)P -> DIV -> FORM -> BODY -> HTML(冒泡阶段)
在事件过程中可以使用
event.eventPhase获取到当前执行事件所处的事件执行阶段。
事件委托
事件的冒泡和捕获提供了事件委托模式。 这种模式适用于:需要为多个元素添加类似的处理事件,为了避免给每个元素绑定事件,可以将处理事件放到这些元素的共同父元素上面。
普通事件委托
如下代码,需要给表格中的每个 <td> 标签添加一个点击事件,使得该单元格背景发生改变。
<table>
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
<td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
</tr>
</table>
这时候可以把点击事件绑定到 <table> 上面, 他将在处理程序中使用 event.target 获取到执行事件的元素。
let selected // 用来存放当前选择的 td 元素
table.addEventListener('click', function(event){
const target = event.target
if(taget.tagName !== 'TD') return //忽略不在 td 上面的点击
if(selected) {
selected.style.backgroundColor = "transparent"
}
selected = target
selected.style.backgroundColor = "red"
})
上面的代码在一定程度上实现了要求,但是当鼠标点击到 <td> 中的 <strong> 标签时, 由于 event.target.tagName 此时并不是 TD 所以会失效。这是可以使用 event.target.closest('tagname') 获取到距离 tagname 标签最近的元素。 修改后的代码为:
let selected // 用来存放当前选择的 td 元素
table.addEventListener('click', function(event){
const td = event.target.closest('td')
if(!td) return // 忽略找不到 td 的元素点击
if(!table.contains(td)) return // 忽略不在 table 当中的 td 元素
if(selected) {
selected.style.backgroundColor = "transparent"
}
selected = target
selected.style.backgroundColor = "red"
})
- 使用
event.target.closest(tagname)返回距离tagname的元素- 使用
ele.contains(eleChild)判断ele元素中是否包含eleChild元素
使用 data- 实现标记委托
如下所示, 存在一个按钮组, 需要给每个按钮添加不同的事件。 这时可以使用 data- 给每个按钮添加一个 action 标记。
<div id="menu">
<button data-action="save">保存</button>
<button data-action="fresh">刷新</button>
<button data-action="search">搜索</button>
</div>
使用事件委托, 把事件绑定到 menu 上用来捕获内部 button 的点击。
const menu = document.getElementById('menu')
function save() {
console.log('save')
}
function fresh() {
console.log('fresh')
}
function search() {
console.log('search')
}
menu.addEventListener('click', (event) => {
const target = event.target.dataset.action
if(!target) return
this[target]()
})
在第一节中,讲到在
handler处理程序中,this指向当前元素, 但是这里我们使用箭头函数,改变函数里面的this指向window, 这是就可以通过this[target]()调用我们在全局定义的方法。
浏览器的默认行为
对于许多元素浏览器赋予了默认功能, 如 <a> 标签点击默认实现路径跳转、表单中的提交按钮点击触发提交行为。这些默认行为并不是符合所有的业务场景的, 这时就需要阻止浏览器的默认行为。
阻止浏览器行为
有两种是实现方式:
- 使用
event.preventDefault()方法。 - 使用
on<event>模式指定事件return false。
<a onclick="return false">click</a>
<a onclick="event.preventDefault()">click</a>
event.preventDefault()方法并不是总是执行的, 还记得在addEventListener中的第三个参数 options。passive用于设置event.preventDefault()是否生效
<a href="https://www.baidu.com" id='aLink'>click</a>
<script>
const aLink = document.getElementById('aLink')
aLink.addEventListener('click', event => {}, {passive: true})
</script>
上面的代码因为在 addEventListener 的第三个参数中设置了 passive: true 导致了 event.preventDefault() 并不能生效
自定义鼠标右击事件
浏览器给鼠标右击默认添加了事件,可以通过 event.preventDefault() 阻止这个默认事件以便自定义右击事件。
<div oncontextmenu="alert(1)">
<button id='btn'></button>
</div>
<script>
const btn = document.getElementById('btn')
btn.addEventListener('contextmenu', event => {
event.preventDefault()
alert(1)
})
</script>
在 btn 元素上绑定了一个 contextmenu 事件, 使用 event.preventDefault() 阻止了浏览器的默认事件, 但是因为事件冒泡右击事件在 btn 元素上执行完毕后又冒泡到了 div 元素上,所以这里也需要使用 event.stopPropagation() 阻止事件冒泡。
const btn = document.getElementById('btn')
btn.addEventListener('contextmenu', event => {
event.preventDefault()
event.stopPropagation()
alert(1)
})
自定义事件
元素上不仅可以分配浏览器内置的事件, 还可以自定义事件分配到元素上。
自定义事件构成
自定义事件需要两个步骤, 第一步是通过 Event 构造器生成一个自定事件, 第二步是在需要的地方通过 dispatchEvent 调用事件。
Event 构造器
let event = new Event(type[, options])
type事件类型, 像click这样的字符串options事件参数bubbles-true/false该事件是否冒泡。true为冒泡。cancelable-true/false该事件是否阻止浏览器默认行为。
默认情况下,
bubbles和cancelable都为false
dispatchEvent
自定义的事件并不能像原生事件那样通过 DOM 的操作触发, 只能通过 diapatchEvent(event) 方法手动调用。
<button id="custom">自定义事件</button>
<script>
const custom = document.getElementById('custom')
custom.addEventListener('myEvent', event => {
console.log(event)
})
const event = new Event('myEvent')
custom.dispatchEvent(event)
</script>
上面的代码包含了自定义事件执行的三个步骤。
- 在元素上注册了一个事件监听
- 生成一个
Event事件实例 - 通过
dispatchEvent调用
自定义事件的冒泡
在上述的代码中自定义事件的调用和事件监听都是通过一个元素进行的, 如果希望父级元素通过冒泡的机制监听事件,可以通过设置 Event 构造器的第二个参数 bubbles: true
<button id="custom">自定义事件</button>
<script>
const custom = document.getElementById('custom')
document.addEventListener('myEvent', event => {
console.log(event)
})
const event = new Event('myEvent', {
bubbles: true
})
custom.dispatchEvent(event)
</script>
值得注意的是, 自定义事件必须通过 addEventListener 注册监听, 并不能通过 on<event> 的方式调用, 因为 on<event> 只能调用内建事件,ele.onMyEvent 并不会执行。
UI 规范事件
实际上 Event 构造函数是所有浏览器事件的基类, 浏览器通过它派生出许多其他规范事件类型, 如 UIEvent FocusEvent MouseEvent WheelEvent KeyboardEvent, 这些事件都基于 Event 构造函数添加了各自的属性和方法。通过这些规范事件构造的实例可以添加一些标准属性。
const event = new MouseEvent('myClick', {
bubbles: true,
cancelable: true,
clientX: 120,
clientY: 200
})
因为 MouseEvent 中包含了 clientX、 clientY 属性, 这使得在自定义事件中可以初始化这些属性, 而这在通过 Event 构造实例时是不起作用的。
为自定义事件添加附加属性
在大部分情况下, 自定义的事件都应该是一个全新的事件类型, 并且应该使用 new CustomEvent 生成一个事件实例。 这样的好处是不必知道该事件应该属于哪种标准规范事件, 并且 CustomEvent 在第二个参数中允许添加附加属性 detail。
<h3 id="h3">Hello Custom Event </h3>
<script>
const h3 = document.getElementById('h3')
h3.addEventListener('hello', event => {
console.log(event.detail.msg)
})
h3.dispatchEvent(new CustomEvent('hello', {
detail: {
msg: 'hello custom'
}
}))
</script>
自定义时间的执行顺序
<button id="menu">Menu (click me)</button>
<script>
menu.addEventListener('click', () => {
menu.dispatchEvent(new CustomEvent('asyncEvent'))
alert(1)
})
menu.addEventListener('asyncEvent', () => {
console.log('2')
})
</script>
上述代码中由于 在 click 事件程序中使用 dispatchEvent 触发了一个自定义事件, 这时函数的执行会立即跳到 addEventListener 注册的监听程序中,当监听程序的代码执行完毕执行完毕后才会再次回到 click 的处理程序中.
所以上面的执行顺序是 2 -> 1