本文作者:一个卷er
前言
React 17.0.0 已经正式发布,无新特性,主要是对一些底层的改进。在 changelog 中 "Use browser focusin and focusout for onFocus and onBlur. (@trueadm in #19186)" 这个改动引起了我的注意。今天这篇文章就来讲讲 focus/blur 和 focusin/focusout 究竟有什么区别。
对于 focus 的理解
focusable
focusable area 是指能够通过用户交互,比如鼠标点击、键盘 tab 键等成为鼠标焦点的元素,或者是开发人员能够使用 focus() 或者 autofocus 属性使得焦点定位到指定的元素。一个 focusable area 通常有以下的一些特性:
- 元素的 tableindex 值是非空的的,当未设置或者值为负数时,focus 只能由 js 控制
- 元素不是禁用状态
- 元素不是正在渲染的内容元素
- css 属性
pointer-events、display不为 none
浏览器中常见的可聚焦的区域有:tab 选项卡、弹出框、表单控件,跳转链接,可编辑区域等。键盘事件通常是发生在这些可聚焦的区域内。
activeElement
当元素处于 focused 状态时,document 的 activeElement 属性指向这个元素,而且这个属性是只读的。在 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 [8]
事件委托
事件委托利用了事件流传递,只指定一个事件处理程序,就可以管理某一类型的所有事件。使用事件委托,只需在 DOM 树种尽量最高的层次添加一个事件处理程序。
焦点事件
DOM3 级事件
焦点事件会在页面元素获得或者失去焦点时触发。DOM3 级支持 4 种焦点事件。它们的对比如下:
| Event | Bubbles | 兼容 |
|---|---|---|
| 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);// 捕获阶段 ✔
兼容处理
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…