React 表单控件 onChange 事件插件

表单专属事件插件,整合各类表单受控组件原生输入 / 变更事件,抹平浏览器输入行为与输入法差异,提供统一、稳定的 onChange 合成事件,支撑 input/textarea/radio/checkbox/select/file 全类型受控表单开发

注册事件:registerEvents

  • 批量注册冒泡 + 捕获双阶段 onChange 事件
  • 统一收口表单变更所有触发渠道:所有能改变表单值、选中态、选区的原生事件全部汇总一处注册
  • 兼顾实时输入、勾选、键盘、选区、失焦全场景 :覆盖文本输入、下拉、单选复选、文件上传、文字选中、焦点切换所有表单交互场景
    • input:文本框、textarea、可编辑元素实时输入触发,用来实现边输入边触发 onChange ,对齐 React 受控组件实时更新语义
    • change:原生标准变更事件,部分浏览器 / 控件仅靠 input 无法覆盖变更场景,兜底使用
      • select 下拉框选中切换
      • file 文件选择
    • clickcheckbox/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 真实 valuestate 绑定值,不一致时强制同步 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

输入合法 HostFiber,安全取出对应的真实 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:用户打字、粘贴、删除,实时触发 onChange
  • change:输入框失去焦点,兜底触发一次变更
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,再匹配 typecheckbox /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 监听避免内存泄漏,清空全局缓存变量 activeElementactiveElementInst,取消当前输入框激活标记

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-xmlcolor-profileSVG 原生带横杠标签
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 失焦时统一同步 stateDOM 值,支持多输入框批量矫正,配合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;
  }
}
相关推荐
濮水大叔4 小时前
Hooks 之后,为什么前端仍需要“包裹”能力?
vue.js·react.js·前端框架
小粉粉hhh4 小时前
React(三)——Redux
前端·javascript·react.js
YFF菲菲兔4 小时前
React 基础事件插件
react.js
qq_529599388 小时前
react js循环滚动
前端·javascript·react.js
索西引擎8 小时前
【React】严格模式:开发阶段质量保障的静态分析机制
前端·react.js·前端框架
小程故事多_801 天前
拆解SOP+ReAct对话Agent,告别硬编码,解锁智能对话的柔性迭代能力
前端·人工智能·react.js·前端框架
皮卡穆1 天前
React 中使用 react-org-tree 实现组织结构树
前端·react.js·前端框架
asdzx671 天前
使用 Spire.WordJS 在 React 中实现在线 Word 文档编辑
前端·react.js·word
kyriewen2 天前
AI 写的 React 组件,合并前必查的 6 个坏味道——每个都有代码对比
前端·javascript·react.js