Appearance
SelectEventPlugin
SelectEventPlugin 是 React 中处理 onSelect 事件的插件。
因为对于原生事件中 onSelect 事件其触发的目标元素只有 Input、Textarea 这两个元素,但是对于 div(contentEditable = 'true') 的元素,原生事件是不支持 onSelect 事件的,所以 React 为了兼容这些不同的事件,将其单独抽离出来,单独处理,从而实现了 div(contentEditable = 'true') 的元素也可以使用 onSelect 事件。
其跟 ChangeEventPlugin 类似,通过 9 种不同的事件类型来模拟 onSelect 事件的触发,从而实现了上述 DOM 元素的统一。
SelectEventPlugin 事件的注册
js
function registerEvents() {
registerTwoPhaseEvent("onSelect", [
"focusout",
"contextmenu",
"dragend",
"focusin",
"keydown",
"keyup",
"mousedown",
"mouseup",
"selectionchange",
]);
}从源码中可以看到, 为了模拟 onSelect 事件,其监听了 "focusout", "contextmenu", "dragend", "focusin", "keydown", "keyup", "mousedown", "mouseup", "selectionchange", 所以当我们在一个 DOM 元素上绑定了 onChange 事件时,其会增加了上述 9 个事件的监听。其中每一个事件的含义是什么?
- focusout: 当元素失去焦点时触发。
- contextmenu: 当用户右键点击元素时触发。
- dragend: 当用户完成元素的拖拽操作时触发。
- focusin: 当元素获得焦点时触发。
- keydown: 当键盘按键被按下时触发。
- keyup: 当键盘按键被松开时触发。
- mousedown: 当鼠标按键被按下时触发。
- mouseup: 当鼠标按键被松开时触发。
- selectionchange: 当元素的文本选择发生改变时触发。
那么当我们在一个 Input 、div(contentEditable = 'true') 的元素上上述 9 个事件的触发流程是什么?
- 鼠标选择事件流程
- 激活 Input 框 触发的事件:
focusin=>mousedown - 鼠标选择内容触发的事件:
selectionchange - 选择完成触发的事件:
mouseup=>focusout
- 键盘选择事件流程
- 激活 Input 框 触发的事件:
mousedown=>focusin - 键盘选择内容触发的事件:
selectionchange=>mouseup=>keydown=>selectionchange=>keyup - 选择完成触发的事件:
keyup=>focusout
为了在 div(contentEditable = 'true') 的元素上也可以使用 onSelect 事件,所以其也模拟上述流程
onSelect 事件的处理流程
SelectEventPlugin 插件提供了 extractEvents 方法,用于处理事件相关的逻辑。其通过 activeElement 和 activeElementInst 来跟踪当前获得焦点的元素, 并用 mouseDown 标志来跟踪用户是否正在拖动选择 , 通过 lastSelection 记录上一次的选择状态,用于比较选择是否发生变化
事件激活
对于 onSelect 事件,其不管是通过 鼠标 还是通过键盘,其都会触发 focusin 事件,从而聚焦到当前元素节点上,所以其事件激活的流程就发生在 focusin事件上
js
switch (domEventName) {
case "focusin":
// 只有 Input 、 Textarea 、 contentEditable === 'true' 元素才会触发 onSelect 事件
if (
isTextInputElement((targetNode: any)) ||
targetNode.contentEditable === "true"
) {
// 记录当前激活的DOM节点和Fiber节点
activeElement = targetNode;
activeElementInst = targetInst;
// 清空上次的选择
lastSelection = null;
}
break;
}这一步也表明了对于 onSelect 事件,其触发的目标只有两种情况:
isTextInputElement((targetNode: any))=>Input、TextareatargetNode.contentEditable === "true"=>div(contentEditable = 'true')
当是这一类元素的时候,其就会记录当前激活的 DOM 节点和 Fiber 节点,从而在后续的事件处理中可以使用到
键盘事件选择
对于通过键盘按键去选择内容的情况,原生事件中触发的流程是 keydown => selectionchange => keyup , 但是 React 合成事件中,并没有通过这三个事件去模拟,而是判断在 keydown、keyup 的时候通过 document.getSelection() 或者 window.getSelection() 来判断是否有选择内容,如果有选择内容,就会触发 selectionchange 事件,从而实现了 onSelect 事件的触发。
js
switch (domEventName) {
case "selectionchange":
if (skipSelectionChangeEvent) {
break;
}
// falls through
case "keydown":
case "keyup":
constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
}鼠标事件选择
对于通过鼠标去选择内容的情况,其会触发 mousedown , contextmenu , mouseup, dragend 这 4 个事件记录用户正在拖动选择的内容, 具体是 当用户按下鼠标的时候触发 mousedown 事件,这时候标记 mouseDown 为 true, 当用户松开鼠标的时候触发 mouseup contextmenu , mouseup, dragend 事件,这时候标记 mouseDown 为 false, 且进行选择事件的判断。即 constructSelectEvent 方法。
js
switch (domEventName) {
case "mousedown":
mouseDown = true;
break;
case "contextmenu":
case "mouseup":
case "dragend":
mouseDown = false;
constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget);
break;
}失去焦点
当触发 focusout 事件的时候,就会将 activeElement 和 activeElementInst 置为 null, 从而避免触发 onSelect 事件。
js
switch (domEventName) {
case "focusout":
activeElement = null;
activeElementInst = null;
lastSelection = null;
break;
}constructSelectEvent
从上面可以看出,对于 onSelect 事件,其真正的核心是 constructSelectEvent 方法,其会排除 鼠标按下、没有激活的元素等元素的意外情况,并通过 getSelection(activeElement) 即通过
- 在支持
selectionStart的时候通过node.selectionStart和node.selectionEnd来判断是否有选择内容,从而触发 onSelect 事件。 - 在不支持
selectionStart的时候通过document.getSelection()或者window.getSelection()来判断是否有选择内容。
并且通过 !lastSelection || !shallowEqual(lastSelection, currentSelection) 来判断选择是否发生变化,从而触发 onSelect 事件。
对于其触发 onSelect 事件就比较简单了,只需要在 dispatchQueue 中添加一个 SelectEvent 事件,然后在 dispatchEvent 中触发 onSelect 事件即可。
js
function constructSelectEvent(dispatchQueue, nativeEvent, nativeEventTarget) {
// 获取 document 文档对象
const doc = getEventTargetDocument(nativeEventTarget);
// 1. 如果是 鼠标选择的方式,在 mousedown 事件触发的时候就会将 mouseDown 置为 true,从而不继续触发 onSelect 事件
// 2. 如果 focuseout 事件触发,则将 activeElement 置为 null,从而不继续触发 onSelect 事件
if (
mouseDown ||
activeElement == null ||
activeElement !== getActiveElement(doc)
) {
return;
}
// Only fire when selection has actually changed.
// 通过 selectionStart 或者 win.getSelection() 方法获取当前的选择
const currentSelection = getSelection(activeElement);
// 判断当前的选择和上次的选择是否一致,如果不一致则触发 onSelect 事件
if (!lastSelection || !shallowEqual(lastSelection, currentSelection)) {
lastSelection = currentSelection;
// 获取所有的事件监听器
const listeners = accumulateTwoPhaseListeners(
activeElementInst,
"onSelect"
);
// 如果有事件监听器,则创建一个 SyntheticEvent 事件对象,并将其放入 dispatchQueue 队列中
if (listeners.length > 0) {
const event = new SyntheticEvent(
"onSelect",
"select",
null,
nativeEvent,
nativeEventTarget
);
dispatchQueue.push({ event, listeners });
// 修改事件对象的 target 属性为 activeElement
event.target = activeElement;
}
}
}