Skip to content

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 个事件的触发流程是什么?

  1. 鼠标选择事件流程
  • 激活 Input 框 触发的事件: focusin => mousedown
  • 鼠标选择内容触发的事件: selectionchange
  • 选择完成触发的事件: mouseup => focusout
  1. 键盘选择事件流程
  • 激活 Input 框 触发的事件: mousedown => focusin
  • 键盘选择内容触发的事件: selectionchange => mouseup => keydown => selectionchange => keyup
  • 选择完成触发的事件: keyup => focusout

为了在 div(contentEditable = 'true') 的元素上也可以使用 onSelect 事件,所以其也模拟上述流程

onSelect 事件的处理流程

SelectEventPlugin 插件提供了 extractEvents 方法,用于处理事件相关的逻辑。其通过 activeElementactiveElementInst 来跟踪当前获得焦点的元素, 并用 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)) => InputTextarea

  • targetNode.contentEditable === "true" => div(contentEditable = 'true')

当是这一类元素的时候,其就会记录当前激活的 DOM 节点和 Fiber 节点,从而在后续的事件处理中可以使用到

键盘事件选择

对于通过键盘按键去选择内容的情况,原生事件中触发的流程是 keydown => selectionchange => keyup , 但是 React 合成事件中,并没有通过这三个事件去模拟,而是判断在 keydownkeyup 的时候通过 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 事件的时候,就会将 activeElementactiveElementInst 置为 null, 从而避免触发 onSelect 事件。

js
switch (domEventName) {
  case "focusout":
    activeElement = null;
    activeElementInst = null;
    lastSelection = null;
    break;
}

constructSelectEvent

从上面可以看出,对于 onSelect 事件,其真正的核心是 constructSelectEvent 方法,其会排除 鼠标按下、没有激活的元素等元素的意外情况,并通过 getSelection(activeElement) 即通过

  • 在支持 selectionStart 的时候通过 node.selectionStartnode.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;
    }
  }
}