浏览器事件
在前端浏览器事件, 我们经常地用到. 例如, onclick 注册一个点击事件. 或者使用 vue @click 去注册等等.
但是, 对于一些浏览器事件的细节, 可能只有到了遇到具体的问题, 才会去了解.
近日, 出现了一个线上的小 bug, 用户在富文本中, 鼠标无法选中.
最后, 发现是 mousedown
冒泡导致的.
也是借着这个 bug, 我总结了一下.
什么是浏览器的事件
Web events are not part of the core JavaScript language — they are defined as part of the APIs built into the browser.
浏览器的事件并不是 JavaScript 的一部分. 它们是内置与浏览器的 API.
常用的浏览器事件有很多, 具体的可以参考 MDN Events
例如: 鼠标点击的事件. 当用户点击某个元素时, 如果我们想要有对应的响应.
就需要给元素注册事件.
怎么注册浏览器的事件
那么, 怎么注册浏览器的事件呢?
- html 中写 onclick 属性注册
- js 中获取到 dom 元素注册
- js 中 addEventListener 注册
html 中写 onclick 属性注册
<button id="btn" onclick="hello">onclick</button>
function hello() {
alert('Hello');
}
这种方式是最为简单粗暴的. 在 html 的属性中直接注册.
但是, 这种方式不推荐. 因为如果有多个元素需要同时注册事件, 则需要多出修改 html 的内容.
另外, 最好 HTML 与 JS 能够分离.
这样更加好组织代码.
js 中获取到 dom 元素注册
const btn = document.querySelector('#btn');
btn.onclick = () => {
alert('B, 第一个 onclick');
};
先获取到 dom 元素, 再将 onclick
属性上添加对应的事件.
此时 JS 与 HTML 没有强相关. 代码组织起来更为方便, 也更容易修改.
但是, 有一个缺点就是无法同时执行多个方法.
js 中 addEventListener 注册
// useCapture 可选, 表示是否采用 捕获的事件处理方式
target.addEventListener(type, listener, useCapture);
addEventListener
的使用方式如上, 其中 useCapture
表示是否采用 捕获的事件处理方式, 具体的细节会在稍后讲解.
我们看一个例子
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
alert('A, addEventListener');
});
btn.addEventListener('click', () => {
alert('B, addEventListener');
});
通过 addEventListener 来注册事件, 则可以分批次多次注册. 执行的顺序和注册的顺序一致.
兼容性: IE8 以及更老版本的 IE 不支持
onclick 与 addEventListener
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
alert('A, addEventListener');
});
btn.onclick = () => {
alert('B, 第一个 onclick');
};
btn.onclick = () => {
alert('C, 第二个 onclick');
};
大家可以想一想, 弹出来的顺序是什么?
如何移除浏览器的事件
讲解了注册事件, 那么如果我不想要了, 怎么移除呢?
onclick 的移除
btn.onclick = null;
很简单, 将 dom 的 onclick
值为空即可.
addEventListener 的移除
addEventListener 移除有对应的方法 removeEventListener
target.removeEventListener(type, listener[, useCapture]);
具体, 我们看一个例子
const btn = document.querySelector('#btn');
btn.addEventListener('click', () => {
alert('A');
});
// 这种方式是无法移除的.
btn.removeEventListener('click', () => {
alert('A');
});
大家注意上述代码是错误的
.
原因是: 虽然执行的代码内容都是一致的, 但 removeEventListener 里的第二个参数, 并不是同一个方法.
此时, 就不能再写成匿名函数了
const btn = document.querySelector('#btn');
function hello() {
alert('hello');
}
btn.addEventListener('click', hello);
btn.removeEventListener('click', hello);
子元素移除父组件的事件
子元素无法直接移除父组件带来的事件.
可以通过 stopPropagation
来阻止事件的冒泡. 具体的细节稍后讲解.
事件的默认行为
当 form 表单中, 只有一个按钮时, 如果点击, 则会出发表单的提交事件.
点击 a 标签, 会默认跳转到新的页面;
等等, 前端中会有一些默认的行为, 有时候这些默认行为是我们不需要的.
我们可以通过在 js 中进行指明.
例如:
<form>
<div>
<label for="fname">First name: </label>
<input id="fname" type="text" />
</div>
<div>
<label for="lname">Last name: </label>
<input id="lname" type="text" />
</div>
<div>
<input id="submit" type="submit" />
</div>
</form>
<p></p>
const form = document.querySelector('form');
const fname = document.getElementById('fname');
const lname = document.getElementById('lname');
const para = document.querySelector('p');
form.onsubmit = function (e) {
if (fname.value === '' || lname.value === '') {
e.preventDefault();
para.textContent = 'You need to fill in both names!';
}
};
这是一个简单的表单验证, 当不满足条件时, 阻止了默认事件, 提交表单.
代码也很简单. e.preventDefault()
事件的冒泡与捕获
子元素和父元素都注册了事件.
当子元素的事件被触发时, 如何处理子组件与父组件之间的事件?
有两种处理方式
- 冒泡 bubbling, 绝大多数浏览器的默认处理方式
- 捕获 capturing, IE 的默认处理方法
我们之前提到的
// useCapture 可选, 表示是否采用 捕获的事件处理方式
target.addEventListener(type, listener, useCapture);
- useCapture 表示是否使用 捕获的处理方法. 默认值是 false
那么, 这两种方法到底是什么意思呢?
我们先看一下, 冒泡
<div id="f1" style="width: 400px; height: 400px; background-color: rgb(235, 156, 156)">
<div id="s1" style="width: 200px; height: 200px; background-color: rgb(98, 98, 223)">s1</div>
</div>
<script>
function add_alert(selector) {
const dom = document.querySelector(selector);
dom.addEventListener(
'click',
() => {
alert(selector);
},
false
);
}
add_alert('#f1');
add_alert('#s1');
</script>
- 如果我们点击粉色的区域, 则会弹出
#f1
, 这是因为 id 为 f1 的元素注册了对应的事件. - 如果我们点击蓝色的区域, 则会先弹出
#s1
再弹出#f1
.
为什么会弹出 #f1
呢? 因为 f1 元素是 s1 元素的父元素. 在父元素中注册的事件, 会在子元素中存在.
如果子元素中有点击, 那么必然会触发父元素的事件.
但是, 我们看到, 会先触发 s1 的事件. 这种情况就是冒泡
. 优先触发子元素的事件, 然后再触发父元素的. 直到 html 标签.
捕获正好相反, 会优先触发 html 的事件, 然后再一层层地触发到子元素.
如果一个元素注册了冒泡
, 也注册了捕获
, 那么, 会先触发捕获, 再触发冒泡.
dom.addEventListener(
'click',
() => {
alert('冒泡');
},
false
);
dom.addEventListener(
'click',
() => {
alert('捕获');
},
true
);
如果同时注册了捕获和冒泡, 则需要分别移除
const btn = document.querySelector('#btn');
function hello() {
alert('hello');
}
btn.addEventListener('click', hello, true);
btn.addEventListener('click', hello, false);
// 需要分别取消hello
btn.removeEventListener('click', hello, true);
btn.removeEventListener('click', hello, false);
如何阻止事件传播
如果说, 子组件不想触发父组件的事件, 则需要停止事件的传播(propagation).
这里的传播包括冒泡和捕获.
<div id="f1" style="width: 400px; height: 400px; background-color: rgb(235, 156, 156)">
<div id="s1" style="width: 200px; height: 200px; background-color: rgb(98, 98, 223)">s1</div>
</div>
<script>
function add_alert(selector) {
const dom = document.querySelector(selector);
dom.addEventListener(
'click',
(e) => {
// 停止事件的传播
e.stopPropagation();
alert(selector);
},
false
);
}
add_alert('#f1');
add_alert('#s1');
</script>
stopPropagation 方法既可以阻止事件冒泡,也可以阻止事件捕获,也可以阻止处于目标阶段。
此时, 点击蓝色区域, 则只会弹出#f1
stopImmediatePropagation 也可以阻止事件的传播. 且同时阻止了其它子元素方法的执行.
具体可以看看 stackoverflow stoppropagation-vs-stopimmediatepropagation