focus/blur VS focusin/focusout

2,664 阅读4分钟

focus-tree.png

本文作者:一个卷er

前言

React 17.0.0 已经正式发布,无新特性,主要是对一些底层的改进。在 changelog 中 "Use browser focusin and focusout for onFocus and onBlur. (@trueadm in #19186)" 这个改动引起了我的注意。今天这篇文章就来讲讲 focus/blurfocusin/focusout 究竟有什么区别。

对于 focus 的理解

focusable

focusable area 是指能够通过用户交互,比如鼠标点击、键盘 tab 键等成为鼠标焦点的元素,或者是开发人员能够使用 focus() 或者 autofocus 属性使得焦点定位到指定的元素。一个 focusable area 通常有以下的一些特性:

  • 元素的 tableindex 值是非空的的,当未设置或者值为负数时,focus 只能由 js 控制
  • 元素不是禁用状态
  • 元素不是正在渲染的内容元素
  • css 属性 pointer-eventsdisplay 不为 none

浏览器中常见的可聚焦的区域有:tab 选项卡、弹出框、表单控件,跳转链接,可编辑区域等。键盘事件通常是发生在这些可聚焦的区域内。

activeElement

当元素处于 focused 状态时,documentactiveElement 属性指向这个元素,而且这个属性是只读的。在 React 中我们可以通过以下方法判断当前文档是否聚焦,或者判断一个元素是否处于 focused 状态:

document.hasFocus(); //boolean
document.activeElement === inputRef.current //boolean

对事件及事件委托的理解

事件流

  • 事件流描述的是页面中接收事件的顺序
  • 事件冒泡(bubble):从文档中嵌套层次最深的那个节点接收,然后逐级向上传播到根节点的文档
  • 事件捕获(capture):从根节点的文档开始接收,然后逐级向下传播到具体的节点
  • DOM2级事件:规定事件流包括三个阶段:
    • 事件捕获阶段
    • 处于目标阶段
    • 事件冒泡阶段 Graphical representation of an event dispatched in a DOM tree using the DOM event flow Graphical representation of an event dispatched in a DOM tree using the DOM event flow [8]

事件委托

事件委托利用了事件流传递,只指定一个事件处理程序,就可以管理某一类型的所有事件。使用事件委托,只需在 DOM 树种尽量最高的层次添加一个事件处理程序。

焦点事件

DOM3 级事件

焦点事件会在页面元素获得或者失去焦点时触发。DOM3 级支持 4 种焦点事件。它们的对比如下:

EventBubbles兼容
focus所有浏览器
focusin低版本的 firefox 不支持
blur所有浏览器
focusout低版本的 firefox 不支持

触发顺序

我们在同时支持四种事件的浏览器中,当焦点在两个元素之间切换时,触发顺序如下(不同浏览器效果可能不同,不必深究):

  • focusin 在第一个目标元素获得焦点前触发
  • focus  在第一个目标元素获得焦点后触发
  • focusout 第一个目标失去焦点时触发
  • focusin 第二个元素获得焦点前触发
  • blur  第一个元素失去焦点时触发
  • focus 第二个元素获得焦点后触发

应用举例

我们有以下的表单控件:

<form id="form">
  <input class="input" name="input"></input>
</form>
  • 使用 focus/blur 或者 focusin/focusout 直接监听输入框
  const form = document.querySelector("#form");
  const input = document.querySelector("#form > .input");
  input.addEventListener("focus", onInputFocus); // ✔
  input.addEventListener("blur", onInputBlur);// ✔
  • 使用 focusin/focusout 直接监听输入框
  const form = document.querySelector("#form");
  const input = document.querySelector("#form > .input");
  input.addEventListener("focusin", onInputFocus); // ✔
  input.addEventListener("focusout", onInputBlur);// ✔
  • 使用 focus/blur 通过事件委托监听父元素(不支持冒泡,需要指定在捕获阶段进行监听)
  const form = document.querySelector("#form");
  form.addEventListener("focus", onInputFocus);// 冒泡阶段 ❌
  form.addEventListener("blur", onInputBlur);// 冒泡阶段 ❌
  form.addEventListener("focus", onInputFocus, true);// 捕获阶段 ✔
  form.addEventListener("blur", onInputBlur, true);// 捕获阶段 ✔
  • focusin/focusout 可以在冒泡或者捕获阶段监听到子元素
  const form = document.querySelector("#form");
  form.addEventListener("focusin", onInputFocus);// 冒泡阶段 ✔
  form.addEventListener("focusout", onInputBlur);// 冒泡阶段 ✔
  form.addEventListener("focusin", onInputFocus, true);// 捕获阶段 ✔
  form.addEventListener("focusout", onInputBlur, true);// 捕获阶段 ✔

codepen 效果示例

兼容处理

zepto 源码中对 focus 事件做的兼容处理:

  • 当支持 focusin 时,使用 focusin/focusout 修正原有的 focus 事件
  • 不支持时,使用 useCapture 在捕获阶段拦截事件
//主要代码
var focusinSupported = 'onfocusin' in window;
var focus = { focus: 'focusin', blur: 'focusout' };

function eventCapture(handler, captureSetting) {
  // 当使用事件代理 & focus 事件 & 不支持 focusin || useCapture ===> 返回 true
  // 否则返回 false
  return handler.del &&
    (!focusinSupported && (handler.e in focus)) ||
    !!captureSetting
}

function realEvent(type) {
  return (focusinSupported && focus[type]) || type
}

总结

在 React 中的 onFocus/onBlur 让人感到困惑的原因是,在原生事件中,这两个 API 并不支持冒泡,但是合成事件通过事件委托监听宿主根节点,并模拟了 事件冒泡-目标-事件捕获 的过程。因此使用 focusin/focusout 代替,能够保证了动作上的一致性,减少用户使用上的困惑。但实际上,是否应该支持 4 种焦点事件呢?

参考资料

[1] React Release 17.0.0: github.com/facebook/re…
[2] React v17.0: reactjs.org/blog/2020/1…
[3] html-spec of focusable-area: html.spec.whatwg.org/multipage/i…
[4] focused: html.spec.whatwg.org/multipage/i…
[5] activeElement: developer.mozilla.org/en-US/docs/…
[6] stackoverflow.com/questions/4…
[7] 事件:《JavaScript 高级程序设计(第3版)》
[8] www.w3.org/TR/DOM-Leve…
[9] www.w3.org/TR/DOM-Leve…
[10] github.com/madrobby/ze…
[11] github.com/facebook/re…