表单专属事件插件,整合各类表单受控组件原生输入 / 变更事件,抹平浏览器输入行为与输入法差异,提供统一、稳定的 onChange 合成事件,支撑 input/textarea/radio/checkbox/select/file 全类型受控表单开发
注册事件:registerEvents
- 批量注册冒泡 + 捕获双阶段
onChange事件 - 统一收口表单变更所有触发渠道:所有能改变表单值、选中态、选区的原生事件全部汇总一处注册
- 兼顾实时输入、勾选、键盘、选区、失焦全场景 :覆盖文本输入、下拉、单选复选、文件上传、文字选中、焦点切换所有表单交互场景
input:文本框、textarea、可编辑元素实时输入触发,用来实现边输入边触发onChange,对齐React受控组件实时更新语义change:原生标准变更事件,部分浏览器 / 控件仅靠input无法覆盖变更场景,兜底使用select下拉框选中切换file文件选择
click:checkbox/radio单选复选框点击勾选 / 取消勾选时触发,原生点击才会改变checked状态keydown / keyup:键盘输入、方向键切换下拉选项、快捷键修改输入内容,捕获键盘操作带来的值变更focusin / focusout:输入框失去焦点、切换焦点场景,部分浏览器仅在失焦才提交变更,配合补齐触发时机selectionchange:输入框文字选中区域变化(光标拖动、选中文字),处理选区变更相关场景
javascript
function registerEvents() {
registerTwoPhaseEvent('onChange', [
'change',
'click',
'focusin',
'focusout',
'input',
'keydown',
'keyup',
'selectionchange',
]);
}
提取事件:extractEvents
根据当前表单控件类型,匹配对应处理工具,找到真正触发变更的 Fiber,生成并收集 onChange 合成事件推入调度队列 ;同时兼容旧浏览器 input 事件垫片、受控组件失焦特殊逻辑
- 获取当前事件对应的
DOM节点,无Fiber兜底window - 使用原生
change事件的控件:适用控件<select>、<input type="file">,这类控件只有原生change能代表值完成变更,对应工具函数只识别change原生事件 - 文本输入框
input /textarea- 现代浏览器:同时识别
input(实时输入)、change(失焦) - 老旧浏览器:垫片模式,需要额外执行
handleEventFunc做兼容修正
- 现代浏览器:同时识别
- 依赖
click触发变更的控件:适用checkbox / radio,点击才会切换checked状态,靠原生click判定变更 - 自定义
Web Component:自定义元素统一走change事件逻辑,和select/file行为对齐 - 若匹配到获取
Fiber工具函数getTargetInstFunc,执行变更事件生成逻辑- 调用对应工具函数
getTargetInstFunc,校验当前原生事件是否真正产生值变更,返回有效 Fiber - 存在有效
Fiber,调用createAndAccumulateChangeEvent创建表单专属合成事件SyntheticChangeEvent,向上遍历收集onChange/onChangeCapture回调,写入调度队列,终止函数,不再执行后续逻辑
- 调用对应工具函数
- 旧浏览器垫片专属处理逻辑:仅
IE等不支持标准input事件时才执行,内部缓存输入框文本、监听keydown/keyup模拟input实时变更行为,补齐浏览器缺失能力 focusout失焦特殊逻辑:原生失去焦点时触发,专门处理受控组件同步矫正 ,受控输入框同步状态,对比DOM真实value与state绑定值,不一致时强制同步DOM值,修复受控输入框光标错位、值滞后问题,解决双向绑定值不同步问题
javascript
function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer,
) {
const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;
let getTargetInstFunc, handleEventFunc;
if (shouldUseChangeEvent(targetNode)) {
getTargetInstFunc = getTargetInstForChangeEvent;
} else if (isTextInputElement(targetNode) {
if (isInputEventSupported) {
getTargetInstFunc = getTargetInstForInputOrChangeEvent;
} else {
getTargetInstFunc = getTargetInstForInputEventPolyfill;
handleEventFunc = handleEventsForInputEventPolyfill;
}
} else if (shouldUseClickEvent(targetNode)) {
getTargetInstFunc = getTargetInstForClickEvent;
} else if (
targetInst &&
isCustomElement(targetInst.elementType, targetInst.memoizedProps)
) {
getTargetInstFunc = getTargetInstForChangeEvent;
}
if (getTargetInstFunc) {
const inst = getTargetInstFunc(domEventName, targetInst);
if (inst) {
createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
nativeEventTarget,
);
return;
}
}
if (handleEventFunc) {
handleEventFunc(domEventName, targetNode, targetInst);
}
if (domEventName === 'focusout' && targetInst) {
const props = targetInst.memoizedProps;
handleControlledInputBlur(targetNode), props);
}
}
辅助函数
获取 Fiber 对应的 DOM:getNodeFromInstance
输入合法 Host 类 Fiber,安全取出对应的真实 DOM 节点
javascript
export function getNodeFromInstance(inst) {
const tag = inst.tag;
if (
// 普通DOM元素 <div> <input>
tag === HostComponent ||
// 可提升的宿主DOM(如title、meta等head内标签)
tag === HostHoistable ||
// 单例宿主(html、body这类全局唯一DOM)
tag === HostSingleton ||
// 文本节点 Text
tag === HostText
) {
return inst.stateNode;
}
throw new Error('getNodeFromInstance: Invalid argument.');
}
区分仅靠 change 代表变更的表单控件:shouldUseChangeEvent
判断当前 DOM 是否为 select 下拉框 / file 文件上传输入框
javascript
function shouldUseChangeEvent(elem) {
const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
return (
nodeName === 'select' ||
(nodeName === 'input' && (elem: any).type === 'file')
);
}
获取 change 事件的目标实例:getTargetInstForChangeEvent
仅当原生事件名是 change 时,返回当前触发事件的 Fiber targetInst
javascript
function getTargetInstForChangeEvent(
domEventName,
targetInst,
) {
if (domEventName === 'change') {
return targetInst;
}
}
判断文本输入控件:isTextInputElement
区分实时文本输入型控件
input类型白名单过滤 :只有白名单内的文本类input才判定为文本输入元素;文件、勾选、按钮类input排除textarea直接判定为文本输入框:多行输入框固定走文本输入专属事件逻辑
javascript
const supportedInputTypes = {
color: true,
date: true,
datetime: true,
'datetime-local': true,
email: true,
month: true,
number: true,
password: true,
range: true,
search: true,
tel: true,
text: true,
time: true,
url: true,
week: true,
};
function isTextInputElement(elem) {
const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
if (nodeName === 'input') {
return !!supportedInputTypes[elem.type];
}
if (nodeName === 'textarea') {
return true;
}
return false;
}
获取文本输入事件的目标实例:getTargetInstForInputOrChangeEvent
文本输入控件专用事件过滤器 :仅给 <input type=text/email/password>、<textarea> 使用,只放行 input/change 两类有效原生事件
input:用户打字、粘贴、删除,实时触发 onChangechange:输入框失去焦点,兜底触发一次变更
javascript
function getTargetInstForInputOrChangeEvent(
domEventName,
targetInst,
) {
if (domEventName === 'input' || domEventName === 'change') {
return getInstIfValueChanged(targetInst);
}
}
模拟 input 实时输入能力:getTargetInstForInputEventPolyfill
模拟现代浏览器 input 实时输入能力,在无原生 input 事件的浏览器上,靠键盘 / 选区事件兜底生成 onChange,保证文本输入实时触发变更事件
javascript
function getTargetInstForInputEventPolyfill(
domEventName,
targetInst,
) {
if (
domEventName === 'selectionchange' ||
domEventName === 'keyup' ||
domEventName === 'keydown'
) {
return getInstIfValueChanged(activeElementInst);
}
}
获取表单控件值变更的实例:getInstIfValueChanged
过滤输入无改动的无效交互 ,避免重复触发 onChange,降低高频打字、光标选区操作的性能开销;统一为文本输入、垫片逻辑提供值变更判断复用能力
- 先通过
Fiber拿到对应真实DOM:调用getNodeFromInstance安全取出输入框DOM,仅支持文本类Host Fiber - 对比新旧值并更新缓存 :
updateValueIfChanged内部对比DOM当前值与框架缓存的上一次值 - 返回有效
Fiber作为变更标记 :值变更则返回传入的targetInst,上层据此创建onChange合成事件;无变化则隐式返回undefined,跳过事件生成
javascript
function getInstIfValueChanged(targetInst) {
const targetNode = getNodeFromInstance(targetInst);
if (updateValueIfChanged(targetNode) {
return targetInst;
}
}
表单控件值变更检测:updateValueIfChanged
为输入框 DOM 绑定专属值追踪器 tracker,对比缓存旧值与当前 DOM 真实值,判断内容是否发生变化
javascript
export function updateValueIfChanged(node) {
if (!node) {
return false;
}
const tracker = getTracker(node);
if (!tracker) {
return true;
}
const lastValue = tracker.getValue();
const nextValue = getValueFromNode(node);
if (nextValue !== lastValue) {
tracker.setValue(nextValue);
return true;
}
return false;
}
提取各类表单控件的值:getValueFromNode
统一提取各类表单 DOM 的状态字符串 ,区分可勾选控件(checkbox/radio)与普通输入控件,输出标准化字符串格式的值
- 勾选类控件:读取
checked,转字符串true/false - 文本 /
select/file:直接读取原生value
javascript
function getValueFromNode(node) {
let value = '';
if (!node) {
return value;
}
if (isCheckable(node)) {
value = node.checked ? 'true' : 'false';
} else {
value = node.value;
}
return value;
}
识别单选 / 复选框表单控件:isCheckable
先校验标签名是 input,再匹配 type 为 checkbox /radio,兼容 DOM 标签大小写
javascript
function isCheckable(elem) {
const type = elem.type;
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(type === 'checkbox' || type === 'radio')
);
}
文本输入垫片的焦点生命周期管理器:handleEventsForInputEventPolyfill
不支持标准 input 事件的老旧浏览器(IE)文本输入垫片,绑定输入框焦点与监听开关,只在输入框激活时跟踪值变化,失焦后释放监听,保证老旧浏览器依靠键盘、选区事件模拟实时 onChange 时数据准确、多输入框互不干扰
focusin输入框获得焦点- 先停止之前所有输入框的值监听
- 开启对当前激活输入框的值变化监听,缓存初始值,用于后续键盘 / 选区事件对比判断内容是否修改
focusout输入框失去焦点:直接关闭全局输入监听,不再跟踪该输入框内容变更
javascript
function handleEventsForInputEventPolyfill(
domEventName,
target,
targetInst,
) {
if (domEventName === 'focusin') {
stopWatchingForValueChange();
startWatchingForValueChange(target, targetInst);
} else if (domEventName === 'focusout') {
stopWatchingForValueChange();
}
}
开启实时值变更监听:startWatchingForValueChange
为旧浏览器文本输入框开启实时值变更监听,替代缺失的标准 input 事件,绑定 IE 专属 propertychange DOM 事件,监听 value 属性修改,捕获打字、粘贴等内容变动
javascript
function startWatchingForValueChange(
target,
targetInst,
) {
activeElement = target;
activeElementInst = targetInst;
activeElement.attachEvent('onpropertychange', handlePropertyChange);
}
关闭实时值变更监听:stopWatchingForValueChange
停止对当前激活输入框的实时值监听,解绑 IE 专属 onpropertychange 事件,释放 DOM 监听避免内存泄漏,清空全局缓存变量 activeElement、activeElementInst,取消当前输入框激活标记
javascript
function stopWatchingForValueChange() {
if (!activeElement) {
return;
}
activeElement.detachEvent('onpropertychange', handlePropertyChange);
activeElement = null;
activeElementInst = null;
}
文本类表单控件值变化监听函数:handlePropertyChange
仅在老旧浏览器文本框激活阶段生效,模拟现代浏览器 input 事件行为,统一 React onChange 跨浏览器表现。过滤 value 属性变动,其余属性修改直接跳过,值变更时手动派发 onChange 合成事件,补齐旧浏览器无原生 input 事件的短板,实现文本输入实时触发变更回调
javascript
function handlePropertyChange(nativeEvent) {
if (nativeEvent.propertyName !== 'value') {
return;
}
if (getInstIfValueChanged(activeElementInst)) {
manualDispatchChangeEvent(nativeEvent);
}
}
手动派发事件:manualDispatchChangeEvent
垫片捕获到输入框 value 变化后调用,主动生成 onChange 合成事件。新建临时调度队列,调用createAndAccumulateChangeEvent构造 SyntheticChangeEvent、收集对应 onChange 回调存入队列,批量更新调度,批量执行队列内所有事件回调,统一走 React 批量更新机制,避免频繁渲染卡顿。补齐老旧浏览器缺失的标准 input 事件能力,让 IE 文本框也能实时触发 onChange,和现代浏览器行为保持一致
javascript
function manualDispatchChangeEvent(nativeEvent) {
const dispatchQueue = [];
createAndAccumulateChangeEvent(
dispatchQueue,
activeElementInst,
nativeEvent,
getEventTarget(nativeEvent),
);
batchedUpdates(runEventInBatch, dispatchQueue);
}
识别单选/复选框表单控件:shouldUseClickEvent
识别 <input type="checkbox">、<input type="radio"> 单选 / 复选框
javascript
function shouldUseClickEvent(elem) {
const nodeName = elem.nodeName;
return (
nodeName &&
nodeName.toLowerCase() === 'input' &&
(elem.type === 'checkbox' || elem.type === 'radio')
);
}
获取 click 事件的目标实例:getTargetInstForClickEvent
专供 checkbox/radio 勾选框使用的事件过滤工具,仅放行原生 click 事件,屏蔽键盘、焦点等无关渠道,返回有效 Fiber
javascript
function getTargetInstForClickEvent(
domEventName,
targetInst,
) {
if (domEventName === 'click') {
return getInstIfValueChanged(targetInst);
}
}
识别 Web 自定义元素:isCustomElement
- 识别 Web 自定义元素(Custom Element) ,标准规范:标签名必须包含短横线
- - 内置 SVG 内置短横线标签黑名单 ,排除
annotation-xml、color-profile等SVG原生带横杠标签
javascript
function isCustomElement(tagName, props) {
if (tagName.indexOf('-') === -1) {
return false;
}
switch (tagName) {
case 'annotation-xml':
case 'color-profile':
case 'font-face':
case 'font-face-src':
case 'font-face-uri':
case 'font-face-format':
case 'font-face-name':
case 'missing-glyph':
return false;
default:
return true;
}
}
Change 事件生成与收集:createAndAccumulateChangeEvent
- 受控输入框状态矫正 :调用
enqueueStateRestore,记录DOM待同步,解决受控组件值滞后、光标错位经典问题 - 收集双阶段回调 :
accumulateTwoPhaseListeners向上收集捕获 + 冒泡两段onChange处理器,和框架其他事件语法统一 - 按需创建合成事件:无监听则直接跳过,节省内存与计算开销;有监听则封装事件与回调存入全局调度队列
javascript
function createAndAccumulateChangeEvent(
dispatchQueue,
inst,
nativeEvent,
target,
) {
enqueueStateRestore(target);
const listeners = accumulateTwoPhaseListeners(inst, 'onChange');
if (listeners.length > 0) {
const event = new SyntheticEvent(
'onChange',
'change',
null,
nativeEvent,
target,
);
dispatchQueue.push({event, listeners});
}
}
受控输入框值恢复任务入队:enqueueStateRestore
缓存需要矫正的输入 DOM,供 focusout 失焦时统一同步 state 与 DOM 值,支持多输入框批量矫正,配合handleControlledInputBlur 在输入框失焦时取出队列 DOM 做值修复,解决受控组件光标错位、值不同步的经典问题,统一管理受控输入框矫正任务,避免多次重复矫正,保证表单双向绑定稳定
javascript
export function enqueueStateRestore(target) {
if (restoreTarget) {
if (restoreQueue) {
restoreQueue.push(target);
} else {
restoreQueue = [target];
}
} else {
restoreTarget = target;
}
}
受控输入框失焦处理:handleControlledInputBlur
数字受控输入框失焦专属兼容修复函数 ,仅处理 number 类型输入框,解决数字输入框原生行为缺陷,即用户输入非法数字(如字母、超出范围值)后,DOM value 会自动清空,导致受控组件状态与 DOM 脱节。失焦时调用 setDefaultValue 将当前 DOM 输入值写入节点默认值属性,维持受控组件 value 同步,避免页面渲染闪烁、数值丢失
javascript
function handleControlledInputBlur(node, props) {
if (node.type !== 'number') {
return;
}
if (!disableInputAttributeSyncing) {
const isControlled = props.value != null;
if (isControlled) {
setDefaultValue(node, 'number', node.value);
}
}
}
更新受控输入框默认值:setDefaultValue
仅为 number 类型受控输入框更新 defaultValue ,过滤其他输入类型;同时仅在输入框非当前激活态时执行,避免编辑中频繁改动默认值干扰输入。统一将传入值转为字符串,对比节点原有 defaultValue,不一致才更新,减少 DOM 属性重复修改
javascript
export function setDefaultValue(
node,
type,
value,
) {
if (
type !== 'number' ||
getActiveElement(node.ownerDocument) !== node
) {
if (node.defaultValue !== toString(value)) {
node.defaultValue = toString(value);
}
}
}
获取当前聚焦激活 DOM 元素:getActiveElement
获取文档当前聚焦激活 DOM 元素 ,自动兜底全局document,无文档环境返回null
javscript
export default function getActiveElement(doc) {
doc = doc || (typeof document !== 'undefined' ? document : undefined);
if (typeof doc === 'undefined') {
return null;
}
try {
return doc.activeElement || doc.body;
} catch (e) {
return doc.body;
}
}