2022 年 4 月 25 日

键盘:keydown 和 keyup

在我们了解键盘之前,请注意,在现代设备上还有其他“输入内容”的方式。例如,人们使用语音识别(尤其是在移动设备上)或使用鼠标进行复制/粘贴。

因此,如果我们希望跟踪 <input> 字段中的任何输入,那么键盘事件是不够的。还有另一个名为 input 的事件,用于通过任何方式跟踪 <input> 字段的更改。对于此类任务,它可能是一个更好的选择。我们将在 事件:change、input、cut、copy、paste 一章中介绍它。

当我们希望处理键盘操作(虚拟键盘也计算在内)时,应该使用键盘事件。例如,对箭头键 向上向下 或热键(包括组合键)做出反应。

测试台

为了更好地理解键盘事件,你可以使用下面的测试台。

尝试在文本字段中输入不同的按键组合。

结果
script.js
style.css
index.html
kinput.onkeydown = kinput.onkeyup = kinput.onkeypress = handle;

let lastTime = Date.now();

function handle(e) {
  if (form.elements[e.type + 'Ignore'].checked) return;

  area.scrollTop = 1e6;

  let text = e.type +
    ' key=' + e.key +
    ' code=' + e.code +
    (e.shiftKey ? ' shiftKey' : '') +
    (e.ctrlKey ? ' ctrlKey' : '') +
    (e.altKey ? ' altKey' : '') +
    (e.metaKey ? ' metaKey' : '') +
    (e.repeat ? ' (repeat)' : '') +
    "\n";

  if (area.value && Date.now() - lastTime > 250) {
    area.value += new Array(81).join('-') + '\n';
  }
  lastTime = Date.now();

  area.value += text;

  if (form.elements[e.type + 'Stop'].checked) {
    e.preventDefault();
  }
}
#kinput {
  font-size: 150%;
  box-sizing: border-box;
  width: 95%;
}

#area {
  width: 95%;
  box-sizing: border-box;
  height: 250px;
  border: 1px solid black;
  display: block;
}

form label {
  display: inline;
  white-space: nowrap;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="style.css">
</head>

<body>

  <form id="form" onsubmit="return false">

    Prevent default for:
    <label>
      <input type="checkbox" name="keydownStop" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
    <label>
      <input type="checkbox" name="keyupStop" value="1"> keyup</label>

    <p>
      Ignore:
      <label>
        <input type="checkbox" name="keydownIgnore" value="1"> keydown</label>&nbsp;&nbsp;&nbsp;
      <label>
        <input type="checkbox" name="keyupIgnore" value="1"> keyup</label>
    </p>

    <p>Focus on the input field and press a key.</p>

    <input type="text" placeholder="Press keys here" id="kinput">

    <textarea id="area" readonly></textarea>
    <input type="button" value="Clear" onclick="area.value = ''" />
  </form>
  <script src="script.js"></script>


</body>
</html>

keydown 和 keyup

当按下某个键时,会触发 keydown 事件,然后在释放该键时触发 keyup 事件。

event.code 和 event.key

事件对象的 key 属性可以获取字符,而事件对象的 code 属性可以获取“物理键码”。

例如,同一个键 Z 可以按住 Shift 键或不按住 Shift 键来按下。这会产生两个不同的字符:小写 z 和大写 Z

event.key 正好是字符,并且是不同的。但 event.code 是相同的。

event.key event.code
Z z(小写) KeyZ
Shift+Z Z(大写) KeyZ

如果用户使用不同的语言,那么切换到另一种语言会产生一个与 "Z" 完全不同的字符。这将成为 event.key 的值,而 event.code 始终相同:"KeyZ"

“KeyZ” 和其他键码

每个键都有一个代码,该代码取决于它在键盘上的位置。键码在 UI 事件代码规范 中进行了描述。

例如

  • 字母键的代码为 "Key<letter>""KeyA""KeyB" 等。
  • 数字键的代码为:"Digit<number>""Digit0""Digit1" 等。
  • 特殊键由其名称编码:"Enter""Backspace""Tab" 等。

有几种广泛使用的键盘布局,并且该规范为每种布局提供了键码。

阅读 规范的字母数字部分 以获取更多代码,或只需在上面的 测试台 中按一个键。

大小写很重要:"KeyZ",而不是 "keyZ"

这似乎很明显,但人们仍然会犯错误。

请避免输入错误:是 KeyZ,而不是 keyZ。像 event.code=="keyZ" 这样的检查不起作用:"Key" 的第一个字母必须大写。

如果某个键不产生任何字符,该怎么办?例如,ShiftF1 或其他键。对于这些键,event.keyevent.code 大致相同。

event.key event.code
F1 F1 F1
退格 退格 退格
Shift Shift ShiftRightShiftLeft

请注意,event.code 准确指定了按下的键。例如,大多数键盘都有两个 Shift 键:一个在左侧,一个在右侧。event.code 准确告诉我们按下了哪一个键,而 event.key 负责键的“含义”:它是什么(一个“Shift”)。

假设我们想要处理一个热键:Ctrl+Z(或 Mac 上的 Cmd+Z)。大多数文本编辑器都会在上面挂接“撤消”操作。我们可以在 keydown 上设置一个侦听器并检查按下的键。

这里有一个两难选择:在这样的侦听器中,我们应该检查 event.key 还是 event.code 的值?

一方面,event.key 的值是一个字符,它会根据语言而改变。如果访问者在操作系统中有多种语言并在它们之间切换,则同一个键会给出不同的字符。因此,检查 event.code 是有意义的,它始终相同。

像这样

document.addEventListener('keydown', function(event) {
  if (event.code == 'KeyZ' && (event.ctrlKey || event.metaKey)) {
    alert('Undo!')
  }
});

另一方面,event.code 存在一个问题。对于不同的键盘布局,同一个键可能具有不同的字符。

例如,以下是美国布局(“QWERTY”)和其下方的德语布局(“QWERTZ”)(来自维基百科)

对于同一个键,美国布局有“Z”,而德语布局有“Y”(字母互换)。

从字面上看,当德语布局的人按下 Y 时,event.code 将等于 KeyZ

如果我们在代码中检查 event.code == 'KeyZ',那么对于德语布局的人,当他们按下 Y 时,此类测试将通过。

这听起来真的很奇怪,但事实就是如此。规范 明确提到了这种行为。

因此,event.code 可能会为意外的布局匹配错误的字符。不同布局中的相同字母可能映射到不同的物理键,从而导致不同的代码。幸运的是,这种情况只发生在几个代码上,例如 keyAkeyQkeyZ(如我们所见),并且不会发生在 Shift 等特殊键上。您可以在 规范 中找到列表。

为了可靠地跟踪与布局相关的字符,event.key 可能是更好的方法。

另一方面,event.code 的好处是始终保持不变,绑定到物理键位置。因此,依赖于它的热键即使在语言切换的情况下也能很好地工作。

我们是否希望处理与布局相关的键?那么event.key是实现这一目标的方法。

或者我们希望在切换语言后仍能使用热键?那么event.code可能会更好。

自动重复

如果一个键被按下足够长的时间,它就会开始“自动重复”:keydown会一次又一次地触发,然后当它被释放时,我们最终会得到keyup。因此,拥有许多keydown和一个keyup是正常的。

对于由自动重复触发的事件,事件对象将event.repeat属性设置为true

默认操作

默认操作各不相同,因为键盘可能引发许多事情。

例如

  • 屏幕上出现一个字符(最明显的结果)。
  • 删除一个字符(Delete键)。
  • 页面滚动(PageDown键)。
  • 浏览器打开“保存页面”对话框(Ctrl+S
  • …等等。

阻止keydown上的默认操作可以取消其中大部分操作,但基于操作系统的特殊键除外。例如,在 Windows 上,Alt+F4会关闭当前浏览器窗口。而且,没有办法通过在 JavaScript 中阻止默认操作来阻止它。

例如,下面的<input>需要一个电话号码,因此它不接受除数字、+()-之外的键

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') || ['+','(',')','-'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

此处的onkeydown处理程序使用checkPhoneKey来检查按下的键。如果它有效(从0..9+-()之一),则返回true,否则返回false

如我们所知,从事件处理程序返回的false值(使用 DOM 属性或属性分配,如上所示)阻止了默认操作,因此对于未通过测试的键,<input>中不会显示任何内容。(返回的true值不会影响任何内容,只有返回false才有意义)

请注意,特殊键(如BackspaceLeftRight)在输入中不起作用。这是严格过滤器checkPhoneKey的副作用。这些键使它返回false

让我们通过允许箭头键LeftRightDeleteBackspace来稍微放松一下过滤器

<script>
function checkPhoneKey(key) {
  return (key >= '0' && key <= '9') ||
    ['+','(',')','-','ArrowLeft','ArrowRight','Delete','Backspace'].includes(key);
}
</script>
<input onkeydown="return checkPhoneKey(event.key)" placeholder="Phone, please" type="tel">

现在箭头和删除可以很好地工作了。

即使我们有键过滤器,人们仍然可以使用鼠标和右键单击 + 粘贴来输入任何内容。移动设备提供了输入值的其他方式。因此,过滤器并不是 100% 可靠的。

替代方法是跟踪 oninput 事件——它在任何修改之后触发。在那里,我们可以检查新的 input.value,并在其无效时修改它/突出显示 <input>。或者我们可以同时使用两个事件处理程序。

传统

过去,有一个 keypress 事件,还有事件对象的 keyCodecharCodewhich 属性。

在使用它们时存在许多浏览器不兼容性,因此规范的开发人员别无他法,只能弃用所有这些属性并创建新的现代事件(如本章前面所述)。旧代码仍然有效,因为浏览器会继续支持它们,但完全没有必要再使用它们了。

移动键盘

在使用正式称为 IME(输入法编辑器)的虚拟/移动键盘时,W3C 标准规定 KeyboardEvent 的 e.keyCode 应为 229e.key 应为 "Unidentified"

虽然其中一些键盘可能仍然对 e.keye.codee.keyCode 等使用正确的值……在按下某些键(如箭头或退格键)时,但并不能保证,因此你的键盘逻辑可能并不总是在移动设备上起作用。

总结

按下键始终会生成键盘事件,无论是符号键还是特殊键,如 ShiftCtrl 等。唯一的例外是 Fn 键,它有时出现在笔记本电脑键盘上。它没有键盘事件,因为它通常在比操作系统更低的级别上实现。

键盘事件

  • keydown——在按下键时(如果长时间按下键,则自动重复),
  • keyup——在释放键时。

主要的键盘事件属性

  • code——“键码”("KeyA""ArrowLeft" 等),特定于键盘上键的物理位置。
  • key——字符("A""a" 等),对于非字符键,如 Esc,通常与 code 的值相同。

过去,键盘事件有时用于跟踪表单字段中的用户输入。这是不可靠的,因为输入可能来自各种来源。我们有 `input` 和 `change` 事件来处理任何输入(将在本章后面介绍 事件:change、input、cut、copy、paste)。它们会在任何类型的输入后触发,包括复制粘贴或语音识别。

当我们真正需要键盘时,我们应该使用键盘事件。例如,对热键或特殊键做出反应。

任务

重要性:5

创建一个函数 `runOnKeys(func, code1, code2, ... code_n)`,在同时按下代码为 `code1`、`code2`、…、`code_n` 的键时运行 `func`。

例如,下面的代码在同时按下 `“Q”` 和 `“W”` 时显示 `alert`(在任何语言中,无论是否启用 CapsLock)

runOnKeys(
  () => alert("Hello!"),
  "KeyQ",
  "KeyW"
);

在新窗口中演示

我们应该使用两个处理程序:`document.onkeydown` 和 `document.onkeyup`。

让我们创建一个集合 `pressed = new Set()` 来保存当前按下的键。

第一个处理程序向其中添加,而第二个处理程序从中删除。每次在 `keydown` 上,我们都会检查是否按下了足够的键,如果是,则运行该函数。

在沙盒中打开解决方案。

教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码 – 将它们包装在 <pre> 标记中,对于 10 行以上的代码 – 使用沙盒(plnkrjsbincodepen…)