onmousedown 为什么不能让 input 聚焦?
这是我参与2022首次更文挑战的第18天,活动详情查看:2022首次更文挑战」。
导语
onmousedown
顾名思义,就是当dom被鼠标按下的时候会触发,他和 onclick
事件的不同在于 onmousedown
仅仅是鼠标被按下的时候会触发,而onclick
事件是整个点击过程,包括鼠标抬起后才会触发,不过这不是本次讨论的重点,关于这一点在稍后也会做出详细的探讨。那么 onmousedown
是否真的不能让 input
聚焦?如果是,那又是什么原因造成的?又要怎么去解决?
一.出现问题
先看以下简短的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">click me</button>
<input type="text" id="input">
</body>
</html>
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onmousedown = function () {
const input = document.getElementById('input')
input.focus()
}
}
</script>
这是问题代码通过精简后的代码,现在看起来非常简单,在 html 中绘制了一个 button
和 一个 input
,在 js 中通过 button 的 onmounsedown
事件监听,去让 input 获得聚焦。从简单的流程理论来看,这么做似乎没有任何问题,来看一下结果:
从页面上看道,input 并没有被聚焦。
所以这就引出来我们要讨论的问题:onmousedown 为什么不能让 input 聚焦?
二.找出原因
如果我们用 onclick
事件去替换:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">click me</button>
<input type="text" id="input">
</body>
</html>
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onclick = function () {
const input = document.getElementById('input')
input.focus()
}
}
</script>
执行结果:
可以发现,问题没有了。所以将 onmousedown
改为 onclick
是解决问题的一个方法。但是这并不代表我们知道是什么原因导致的,假如面试中遇到面试官的提问你又能怎么回答,又或者我就是想用 onmousedown
去实现这个功能又能怎么办呢?所以找出原因才是解决问题的关键。
三. 尝试猜想
在浏览器中,一个页面只可能存在一个组件被聚焦,那么我们是不是可以猜想一下,当前 input 没有被聚焦的原因是焦点被聚焦到了别的dom上,而整个页面总共只有2个组件,所以我们可以尝试去监听一下 button 的聚焦行为。
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onfocus = function () {
console.log('button focused')
}
button.onmousedown = function () {
const input = document.getElementById('input')
input.focus()
}
}
</script>
代码切回onmousedown
,在这里,我们调用了 button 的 onfocus
方法,从而监听 button 的聚焦情况:
果然不出所料地执行了 button 的 focus 回调,说明当我调用 button 的时候 onmousedown
的时候,JS随后会继续派发 focuse
方法,从而让 button 获取焦点。
那么整个流程就比较清晰了:
猜想结论:
-
用户点击了按钮,那么
onmousedown
方法将会被执行 -
在
onmousedown
方法中,我们调用了input.focus()
方法,让 input 获取焦点。 -
button
onmousedown
方法执行完后,会自动执行 button.focus()从而让 button获取焦点。 -
button 获取焦点就会导致 input 失焦,从而页面展示与预期不同。
四. 验证猜想
1. 打印流程
最简单的办法,就是我们在执行的每一个关键流程点打一个桩,然后通过打桩的打印顺序来判断是否跟我们想象的一样。
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onfocus = function () {
console.log('button focus')
}
button.onmousedown = function () {
console.log('button onmousedown')
const input = document.getElementById('input')
input.focus = function () {
console.log('input focus')
}
input.focus()
}
}
</script>
这里打印了3个节点,分别是 button 的 onmousedown 和 focus,以及 input 的 focus。
结果可以发现和我们的猜想一致,确实是 input 获取了焦点之后,焦点被 button 夺走,页面的 input 将不再是获焦状态。
所以出现这个问题的本质上不是因为 onmousedown 让 input 获取不到焦点,而是因为 input 在获取焦点之后 onmousedown 又重新夺走了 input 的焦点!
2. Performance 录制
为了避免混乱,我们仅仅保留 onmousedown 的方法结构,内容全部删掉:
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onmousedown = function () {
}
}
</script>
开启录制:
如果动图过大加载不出来可以看下下面的截图:
确实可以看到 button 在执行 onmousedown 方法之后又执行了 focus ,到这里因该就可以实锤我们的猜想了。
五. 解决问题
到目前为止,我们已经知道问题发生的原因了,现在我们来考虑如何解决问题。当然,依然是在使用 onmousedown
的情况下让 input 保持聚焦。
1. 让 button 失去聚焦事件
采用 preventDefault()
取消事件
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onmousedown = function (event) {
event.preventDefault()
const input = document.getElementById('input')
input.focus()
}
}
</script>
实践证明是可以解决问题,大家可以自行尝试。
2. 利用事件循环
先上代码:
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onmousedown = function (event) {
const input = document.getElementById('input')
setTimeout(() => {
input.focus()
}, 0);
}
}
</script>
在代码中仅仅添加了一个定时器就可以解决问题,注意这里定时器延迟时间是0,也就是说理论上并没有延迟时间。但是可以解决问题,这是为什么呢?其实是利用的时间循环机制,关键不在于定时器延迟的时间是多少,而是在于定时器是异步的,JS会将异步的任务挂起,并推送到任务队列中,在主线程任务执行完毕后,会去查看任务队列中的任务并开始执行。
六.为什么 onclick 不会出现问题
在讨论的最开始,我们尝试使用 onclick 事件去代替 onmousedown 事件,发现这个 Bug 就不会出现,那么这是为什么呢?同样的,归根到底还是执行顺序的问题。
我们可以简单地理解为: onmousedown 鼠标按下去就立即出发,而 onclick 还需要一个 鼠标 抬起的动作(onmouseup) 。所以,我们也可以认为, onclick 的事件涵盖了 onmousedown,onmousedown 是 onclick的前提。
既然如此,我们在 onmousedown 中会自动调用 focus ,在 onclick事件中从设计上来说就没有重复调用的必要了。
<script type="text/javascript">
window.onload = function () {
const button = document.getElementById('btn')
button.onfocus = function () {
console.log('button focus')
}
button.onmousedown = function () {
console.log('button onmousedown')
const input = document.getElementById('input')
input.focus = function () {
console.log('input focus')
}
input.focus()
}
button.onclick = function () {
console.log('button click')
}
button.onmouseup = function () {
console.log('button onmouseup')
}
}
</script>
打印结果:
上面是我按下按钮之后所响应的打印结果,从上面可以看出,不仅仅是 onclick,只要是 button focus
之后的节点,理论上都是可以作为让 input 聚焦的时间节点的,例如 mouseup。。。只不过看起来有点别扭。