Appearance
BeforeInputEventPlugin
BeforeInputEventPlugin 是 React 中处理 onBeforeInput 、 onCompositionEnd、 onCompositionStart、 onCompositionUpdate 事件的插件。
composition 类型事件
对于这 3 个事件,我们要了解一下这几个事件的作用及其触发条件
原生事件
当用户使用拼音输入法开始输入汉字时,compositionstart事件就会被触发。当文本段落的组成完成或取消时, compositionend 事件将被触发。
也就是说,在我们开始输入中文的时候会触发一次compositionstart事件,中文输入过程中不会再出发compositionstart事件,最后输入中文完成触发compositionend 事件。 compositionstart先于input事件触发。
那么为什么需要重写这个事件?还是因为这个事件存在兼容性问题,部分浏览器不支持。
所以我们就需要根据以下情况进行处理
- 原生事件就支持
通过 const canUseCompositionEvent = canUseDOM && 'CompositionEvent' in window; 判断浏览器是否原生就支持这三个事件,如果支持,就直接使用原生的事件
- 原生不支持
从前面原生事件发现,其核心是需要判断是否是在输入法状态,那么怎么判断的呐?
- 进入输入法状态
在 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;
}
}输入值的获取
下面还需要处理的就是 事件触发的时候关键的 值的获取,其同样分为两种
- 原生事件就支持
原生支持的情况下就简单了,直接通过 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;
}- 原生不支持
原生不支持的情况,我们是通过 keypress 事件触发的时候,判断是否是输入法状态下,所以这时候也简单,具体在 onBeforeInput 中说明了
onBeforeInput 事件
对于这个事件,我们先要了解一下 textInput keypress 等几个事件
textInput
DOM3 级事件引人了一个新事件——textInput,用来替代 keypress 事件。当用户在可编辑区域中输入字符时,就会触发这个事件
注意该事件只支持 DOM2 级事件处理程序,且只有 chrome 和 safari 浏览器支持
textInput 与 keypress 事件有两点不同
textInput事件只会在用户按下能够输入实际字符的键时才会被触发,而 keypress 事件则在按下那些能够影响文本显示的键时也会触发(如回车键)任何可以获得焦点的元素都可以触发 keypress 事件,但只有可编辑区域才能触发 textInput 事件
在 IME 中,textInput 事件会在用户输入字符时触发,而 keypress 事件不会触发
beforeinput
beforeinput 事件是 textInput 事件的一个扩展,它会在用户输入字符之前触发,并且可以阻止默认行为
但是其跟 textInput 事件有一点不同,
textInput事件是在用户输入字符之后触发,而beforeinput事件是在用户输入字符之前触发- IME 情况下,
textInput事件是在用户真正输入字符的时候才触发,beforeinput则在每一个字符 和 选择内容的时候都触发
所以当我们输入一个 好

发现 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事件并分以下两种情况:
- 支持 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;
}
}- 不支持 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);
}