Skip to content

BeforeInputEventPlugin

BeforeInputEventPlugin 是 React 中处理 onBeforeInputonCompositionEndonCompositionStartonCompositionUpdate 事件的插件。

composition 类型事件

对于这 3 个事件,我们要了解一下这几个事件的作用及其触发条件

原生事件

当用户使用拼音输入法开始输入汉字时,compositionstart事件就会被触发。当文本段落的组成完成或取消时, compositionend 事件将被触发。

也就是说,在我们开始输入中文的时候会触发一次compositionstart事件,中文输入过程中不会再出发compositionstart事件,最后输入中文完成触发compositionend 事件。 compositionstart先于input事件触发。

那么为什么需要重写这个事件?还是因为这个事件存在兼容性问题,部分浏览器不支持。

所以我们就需要根据以下情况进行处理

  1. 原生事件就支持

通过 const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window; 判断浏览器是否原生就支持这三个事件,如果支持,就直接使用原生的事件

  1. 原生不支持

从前面原生事件发现,其核心是需要判断是否是在输入法状态,那么怎么判断的呐?

  • 进入输入法状态

在 IE 等不支持标准 composition 事件的浏览器中使用,通过 keydown 事件 和 keyCode === START_KEYCODE(229) 判断是 compositionStart 状态

js
function isFallbackCompositionStart(
  domEventName: DOMEventName,
  nativeEvent: any
): boolean {
  // 检查是否是keydown事件且keyCode等于START_KEYCODE(229)
  // 在IE中,当输入法开始组合输入时会触发keydown事件且keyCode为229
  return domEventName === "keydown" && nativeEvent.keyCode === START_KEYCODE;
}
  • 离开输入法状态

    • 对于 keyup 事件,检查是否是 Tab/Return/Esc/Space 键(END_KEYCODES)
    • 对于 keydown 事件,检查 keyCode 是否不等于 START_KEYCODE(229)
    • 对于 keypress/mousedown/focusout 事件,直接返回 true
js
function isFallbackCompositionEnd(
  domEventName: DOMEventName,
  nativeEvent: any
): boolean {
  switch (domEventName) {
    case "keyup":
      // Command keys insert or clear IME input.
      return END_KEYCODES.indexOf(nativeEvent.keyCode) !== -1;
    case "keydown":
      // Expect IME keyCode on each keydown. If we get any other
      // code we must have exited earlier.
      return nativeEvent.keyCode !== START_KEYCODE;
    case "keypress":
    case "mousedown":
    case "focusout":
      // Events are not possible without cancelling IME.
      return true;
    default:
      return false;
  }
}

输入值的获取

下面还需要处理的就是 事件触发的时候关键的 值的获取,其同样分为两种

  1. 原生事件就支持

原生支持的情况下就简单了,直接通过 nativeEvent.detail.data 去获取

js
const customData = getDataFromCustomEvent(nativeEvent);
if (customData !== null) {
  event.data = customData;
}

function getDataFromCustomEvent(nativeEvent: any) {
  const detail = nativeEvent.detail;
  if (typeof detail === "object" && "data" in detail) {
    return detail.data;
  }
  return null;
}
  1. 原生不支持

原生不支持的情况,我们是通过 keypress 事件触发的时候,判断是否是输入法状态下,所以这时候也简单,具体在 onBeforeInput 中说明了

onBeforeInput 事件

对于这个事件,我们先要了解一下 textInput keypress 等几个事件

textInput

DOM3 级事件引人了一个新事件——textInput,用来替代 keypress 事件。当用户在可编辑区域中输入字符时,就会触发这个事件

注意该事件只支持 DOM2 级事件处理程序,且只有 chrome 和 safari 浏览器支持

textInputkeypress 事件有两点不同

  1. textInput 事件只会在用户按下能够输入实际字符的键时才会被触发,而 keypress 事件则在按下那些能够影响文本显示的键时也会触发(如回车键)

  2. 任何可以获得焦点的元素都可以触发 keypress 事件,但只有可编辑区域才能触发 textInput 事件

  3. 在 IME 中,textInput 事件会在用户输入字符时触发,而 keypress 事件不会触发

beforeinput

beforeinput 事件是 textInput 事件的一个扩展,它会在用户输入字符之前触发,并且可以阻止默认行为

但是其跟 textInput 事件有一点不同,

  1. textInput 事件是在用户输入字符之后触发,而 beforeinput 事件是在用户输入字符之前触发
  2. IME 情况下, textInput事件是在用户真正输入字符的时候才触发, beforeinput 则在每一个字符 和 选择内容的时候都触发

所以当我们输入一个

image-20250611163331089

发现 beforeinput 时间触发了 4 次 (h,a,o,) ,但是 textInput 事件只触发了一次 ()

为了解决浏览器中 beforeinput 事件在 IME 中多次触发的问题,React 对于这个事件进行了封装,将其注册成原生事件中其他四个事件进行模拟,其分别是 'compositionend', 'keypress', 'textInput', 'paste'

onBeforeInput 事件流程

因为 onBeforeInput 事件模拟的最理想原生事件是 textInput事件,但是textInput 事件是 DOM3 级事件,某些浏览器不支持,所以其需要在不支持的情况下 通过 keypress事件来模拟,所以其可以根据 const canUseTextInputEvent = canUseDOM && 'TextEvent' in window && !documentMode;来判断是否支持 textInput事件并分以下两种情况:

  1. 支持 textInput 事件

对于支持 textInput 事件的时候,其就使用 textInput 事件去获取最新输入的字符,但是其中有一个特殊的情况,就是空格键 :在 Webkit 中,阻止 textInput 事件的默认行为会取消字符插入,同时也会触发浏览器的默认空格键行为(页面滚动),所以其将空格键交于 keypress 事件进行处理

js
function getNativeBeforeInputChars(
  domEventName: DOMEventName,
  nativeEvent: any
): ?string {
  switch (domEventName) {
    case "compositionend":
      return getDataFromCustomEvent(nativeEvent);
    case "keypress":
      // 处理keypress事件
      // 如果有原生textInput事件可用,React会优先使用它们
      // 但空格键是一个特殊情况:在Webkit中,阻止textInput事件的默认行为
      // 会取消字符插入,同时也会触发浏览器的默认空格键行为(页面滚动)
      const which = nativeEvent.which;
      // 如果不是空格键,不做处理,交给 textInput 处理
      if (which !== SPACEBAR_CODE) {
        return null;
      }
      // 如果是空格键,
      hasSpaceKeypress = true;
      return SPACEBAR_CHAR;

    case "textInput":
      // 记录textInput事件中要添加到DOM中的字符
      const chars = nativeEvent.data;
      // 如果是空格键字符,那么我们已经在keypress级别处理过它,直接返回null
      if (chars === SPACEBAR_CHAR && hasSpaceKeypress) {
        return null;
      }
      // 返回新输入的字符
      return chars;

    default:
      // For other native event types, do nothing.
      return null;
  }
}
  1. 不支持 textInput 事件

那么这时候就需要通过 keypress 事件去获取最新输入的字符,但是这个事件在 IME(输入法)情况下会触发多次,所以其通过 isComposing 去判断是否在输入法情况下,判断逻辑如下

js
if (!isComposing && eventType === "onCompositionStart") {
  isComposing = FallbackCompositionStateInitialize(nativeEventTarget);
}

即当触发了 onCompositionStart 事件的时候,就将 isComposing 设置为 true, 从而知道了当前是否在输入法情况下。

  • 在输入法的情况下,其只有在 onCompositionEnd的时候才进行事件内容的处理,从而避免了 keypress 事件的循环触发,

那么其怎么获取最新输入的字符呐?

其实很简单,其在 onCompositionStart 的时候记录当前元素上初始的文本内容(startText)

当触发 onCompositionEnd,再次获取 node.value 的值作为 endValue,通过两次遍历找到新旧值的差异

js
export function getData() {
  // 如果缓存过一次,那么就直接获取缓存的内容
  if (fallbackText) {
    return fallbackText;
  }

  let start;
  // 获取 compositionstart 触发时记录的初始文本内容
  const startValue = startText;
  const startLength = startValue.length;
  let end;
  // 获取 compositionend 触发时当前的文本内容
  const endValue = getText();
  const endLength = endValue.length;
  // 第一次遍历获取新旧文本第一个不同的下标
  for (start = 0; start < startLength; start++) {
    if (startValue[start] !== endValue[start]) {
      break;
    }
  }
  // 获取更新后添加的内容
  // 第二次: 从第一个不同的下标去遍历,如果 minEnd > 0 那么就获取到最大的不同下标
  const minEnd = startLength - start;
  for (end = 1; end <= minEnd; end++) {
    if (startValue[startLength - end] !== endValue[endLength - end]) {
      break;
    }
  }
  // 如果 end 大于1 说明是有新的值输入了
  // 如果小于0,那么说明就是删除
  const sliceTail = end > 1 ? 1 - end : undefined;
  // 缓存起来
  fallbackText = endValue.slice(start, sliceTail);
  return fallbackText;
}
  • 在组合键的情况

通过判断是否是组合键,去剔除组合键问题,即通过 !isKeypressCommand(nativeEvent) 判断

  • 其他情况

对于其他情况,那么就比较简单的,就是直接可以复用 keypress 事件触发的值,但是需要注意的时候 这个值是 charCode, 所以需要解码

js
if (nativeEvent.char && nativeEvent.char.length > 1) {
  return nativeEvent.char;
} else if (nativeEvent.which) {
  return String.fromCharCode(nativeEvent.which);
}