onChange事件详解

React 中,onChange 事件用于在 input、textarea,select,radio 等元素中监听的变化,是我们使用频率非常高的事件。下面将带你详细了解onChange事件的详细原理。

onChange 事件的组成

对于一个绑定了 onChange 事件的元素,我们知道,React 在创建元素的时候,该事件会被委托给 Document 对象来处理。React 会通过事件名称获的真正需要绑定的事件。比如 onClick 事件依赖的就是 click。而 onChange 事件则比较复杂,它足足依赖了 8 个事件,我们通过 ChangeEventPlugin 事件插件,可以看出这 8 个事件依次是 blur,change,click,focus,input,keydown,keyup,selectionchange。

react-dom\src\events\ChangeEventPlugin.js

const eventTypes = {
  change: {
    phasedRegistrationNames: {
      bubbled: 'onChange',
      captured: 'onChangeCapture',
    },
    dependencies: [
      TOP_BLUR, // blur
      TOP_CHANGE, // change
      TOP_CLICK, // click
      TOP_FOCUS, // focus
      TOP_INPUT, // input
      TOP_KEY_DOWN, // keydown
      TOP_KEY_UP, // keyup
      TOP_SELECTION_CHANGE, // selectionchange
    ],
  },
};

所以,当你在 React 中为一个元素增加了 onChange 监听时,实际上,你为 Document 对象增加了 8 个监听。

  • blur:当一个元素失去焦点的时候 blur 事件被触发。
  • change:当一个元素的值发生更改后,在失去焦点的时候触发(在 blur 事件之前)。
  • click:鼠标在一个元素上被按下和放开时。
  • focus:在元素获取焦点时触发。
  • input:当一个 <input>, <select>, 或 <textarea> 元素的 value 被修改时,会触发 input 事件。
  • keydown:键盘按下。
  • keyup:键盘松开。
  • selectionchange:在文档上的当前文本选择被改变时触发(实测光标移动也会触发这个事件)。

那么此时的你肯定就会疑惑了,这么多的事件,其中有很多事件都是在一次动作中触发的,比如一次点击,很有可能同时触发 click 和 focus。那 onChange 监听岂不是会重复调用?怀着这样的疑问,我带着你从头到尾梳理一下 onChange 的调用过程。

onChange 事件触发流程

为了方便理解,我们写一个简单的 input 框,为其绑定 onChange 事件。

export default class ChangeDemo extends React.Component {
    handlerChange = (e) => {
        console.log("change", e)
    };

    render() {
        return (
            <input onChange={this.handlerChange} type="text" placeholder="用户名" />
        )
    }
}

假设一个普通的文本输入场景,我进入页面,点击输入框,光标开始闪烁,我通过键盘输入文字,然后点击文本框外的区域,光标消失。这个场景可切分为三个步骤。

  1. 点击文本框,会依次触发原生事件 focus -> selectionchange -> click。
  2. 输入一个字母,会触发事件 keydown -> input -> selectionchange -> keyup。
  3. 点击输入框以外的区域,触发事件 change -> blur -> selectionchange -> click。
    以上的三个步骤中,只有步骤二,也就是输入文本时,会触发 onChange 事件的回调,步骤一和步骤三都没有触发回调。

SimpleEventPlugin 插件失效

元素建立的时候全局增加了 8 个监听,但是当点击文本框的时候, focus 事件没有触发 onChange 事件的回调,为什么呢?

首先分析 SimpleEventPlugin 为什么没有触发 onChange 回调。

我们知道,事件是由 React 事件插件管理的,多个事件插件会依序对 nativeEvent(原生事件)进行处理。 SimpleEventPlugin 位于插件中的第一位(在浏览器环境下是首位)。恰巧,SimpleEventPlugin 就是处理 focus 等简单事件的。

在 SimpleEventPlugin 的 extractEvents 方法中,我们确认 focus 事件会被转化为一个 SyntheticFocusEvent 合成事件。

case DOMTopLevelEventTypes.TOP_BLUR:
case DOMTopLevelEventTypes.TOP_FOCUS:
  EventConstructor = SyntheticFocusEvent;
  break;
SyntheticFocusEvent 事件建立完成后,会为这个事件增加监听,方法如下。

export function getListener(inst: Fiber, registrationName: string) {
  let listener;
  ...
  const stateNode = inst.stateNode;
  const props = getFiberCurrentPropsFromNode(stateNode);

  listener = props[registrationName];
  ...
  return listener;
}

这里我们发现,SimpleEventPlugin 插件获得监听的方式其实是根据属性名来的,比如 focus 的话,就去找元素上找属性名是 onFocusCapture(focus 事件没有冒泡)的方法。以此作为该事件的回调。我们这里只绑定了 onChange 方法,所以 focus 事件触发后,SimpleEventPlugin 其实是创建了 SyntheticFocusEvent,但是该 event 上的回调事件为空。

综上,SimpleEventPlugin 插件并不是失效,它已经监听到了 focus 事件,也创建了对应的 SyntheticFocusEvent,只是在获取 foucs 的回调函数的时候,按照插件的规则没有获得 onChange 回调函数。 SimpleEventPlugin 的 eventTypes 并没有包含 change 事件,这也就表明,SimpleEventPlugin 插件永远不会触发 onChange 回调函数。

SimpleEventPlugin 插件执行完毕后,就是 ChangeEventPlugin 插件,这个插件是专门处理 onChange 事件的。

onChage 事件触发元素

ChangeEventPlugin 插件会处理三类元素。

第一类是 select 选择列表或者是 file 上传框。这一类元素的特点是无需输入。

对于这类元素,ChangeEventPlugin 插件只关注 change 事件。对于 select 元素,change 事件会在选择某个选项时发生,对于 file 上传框,change 事件在选择文件后触发。可见,对于这一类元素,只需要 change 事件就可以监听元素的改变,剩余的 7 个事件不需要关心。

function shouldUseChangeEvent(elem) {
  const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
  return (
    nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
  );
}

第二类是 input 文本框或 textarea 多行文本框,input 由于 type 这个属性,表现完全不同,所以会通过 supportedInputTypes 对象对 input 的 type 属性做判断,确保该元素是用于文本输入的。

function isTextInputElement(elem: ?HTMLElement): boolean {
  const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();

  if (nodeName === 'input') {
    return !!supportedInputTypes[((elem: any): HTMLInputElement).type];
  }

  if (nodeName === 'textarea') {
    return true;
  }

  return false;
}

const supportedInputTypes: {[key: string]: true | void} = {
  ...
  number: true,
  password: true,
  text: true,
  time: true,
  ...
};

对于文本框类型的元素,ChangeEventPlugin 插件会关注 input 事件和 change 事件。不仅如此,为了避免 onChange 回调函数重复调用,该插件会还判断此次事件是否会导致 value 值的变化。只有 value 发生了变化,才会触发回调。

React 是如何知道 value 值是否变化的? 简单来讲,React 在创建 input 或 textarea 的时候,会调用 track 方法,该方法中会新建一个 tracker 对象,使用代理的方式拦截 value 值的 get 和 set 方法,然后将最新值记录在 tracker 中,该对象挂载到 node 节点上。这样,只要比较事件中的 value 和 tracker 中的 value,就知道本次事件是否会导致 value 的变化。

switch (tag) {
  case 'input':
    track((domElement: any));
    ...
    break;
  case 'textarea':
    track((domElement: any));
    ...
    break;

第三类元素就是 checkbox 和 radio。这一类元素的特点也很明显,它们都是通过 click 事件来触发变更的。所以,对于这类元素,ChangeEventPlugin 插件会关注 click 事件,当然,由于 checkbox 和 radio 都属于 radio,节点对象上也有一个 tracker,插件会还判断此次事件是否改变了 checked 值的变化。

看源码中注释会发现,checkbox 和 radio 不用 change 的原因是由于 change 事件在 IE8 中只有 blur 的时候会触发,而 click 则在任意浏览器都支持。

function shouldUseClickEvent(elem) {
  // Use the `click` event to detect changes to checkbox and radio inputs.
  // This approach works across all browsers, whereas `change` does not fire
  // until `blur` in IE8.
  const nodeName = elem.nodeName;
  return (
    nodeName &&
    nodeName.toLowerCase() === 'input' &&
    (elem.type === 'checkbox' || elem.type === 'radio')
  );
}

到此为止,我们分析了 ChangeEventPlugin 插件处理的三类元素,以及这三类元素触发的事件。

select 和 file 监听 change 事件,input 和 textarea 监听 input 和 change 事件,checkbox 和 radio 监听 click 事件。

有没有发现,keyup,keydown,selectionchange,focus, blur 这五个事件无人问津。其实是这样的,input 事件在 IE8 和 IE9 中不兼容。插件会判断当前浏览器是否支持 input 事件,不支持就使用这五个事件来模拟。

onChange 事件合成

经过上述的层层筛选后,ChangeEventPlugin 终于开始创建 SyntheticEvent 对象。它直接使用 SyntheticEvent.getPooled 方法获得合成事件对象。注意 eventTypes.change,就是我们上面分析过的 onChange 事件的配置,后续后根据该对象获得 onChange 或者是 onChangeCapture 回调函数。

function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
  const event = SyntheticEvent.getPooled(
    eventTypes.change,
    inst,
    nativeEvent,
    target,
  );
  event.type = 'change';
  // Flag this event loop as needing state restore.
  enqueueStateRestore(target);
  accumulateTwoPhaseDispatches(event);
  return event;
}

accumulateTwoPhaseDispatches 方法用于模拟事件的捕获和冒泡,之前详细讲过,这里就不赘述了。

总结

  1. React 中,onChagne 事件是一个合成事件,由 ChangeEventPlugin 插件处理其监听。
  2. ChangeEventPlugin 插件会处理三类元素,select 和 file 监听 change 事件,input 和 textarea 监听 input 和 change 事件,checkbox 和 radio 监听 click 事件。
  3. keyup,keydown,selectionchange,focus, blur 这五个事件是为了模拟 input 事件,用于兼容 IE8 和 IE9。个人感觉这块可以优化。
  4. 事件的执行顺序,取决了插件的位置,click 事件被 SimpleEventPlugin 插件处理,其回调函数优先于 onChange 回调函数。

参考文献

React 事件 | 1.React中的事件委托

React 事件 | 2. React 中的事件插件

React 事件 | 3. React 中的事件对象

React 事件 | 4. React 事件监听

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值