onmousedown 为什么不能让 input 聚焦?

450 阅读5分钟

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 获得聚焦。从简单的流程理论来看,这么做似乎没有任何问题,来看一下结果:

1_1.gif

从页面上看道,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>

执行结果:

1_2.gif

可以发现,问题没有了。所以将 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 的聚焦情况:

1_3.gif

果然不出所料地执行了 button 的 focus 回调,说明当我调用 button 的时候 onmousedown的时候,JS随后会继续派发 focuse方法,从而让 button 获取焦点。

那么整个流程就比较清晰了:

1.gif

猜想结论:

  1. 用户点击了按钮,那么 onmousedown方法将会被执行

  2. onmousedown 方法中,我们调用了 input.focus()方法,让 input 获取焦点。

  3. button onmousedown方法执行完后,会自动执行 button.focus()从而让 button获取焦点。

  4. 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。

1_4.gif

结果可以发现和我们的猜想一致,确实是 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>

开启录制:

1_5.gif

如果动图过大加载不出来可以看下下面的截图:

image.png

确实可以看到 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>

打印结果:

image.png

上面是我按下按钮之后所响应的打印结果,从上面可以看出,不仅仅是 onclick,只要是 button focus之后的节点,理论上都是可以作为让 input 聚焦的时间节点的,例如 mouseup。。。只不过看起来有点别扭。