React16源码: React中event事件对象的创建过程源码实现

event 对象

1 ) 概述

  • 在生产事件对象的过程当中,要去调用每一个 possiblePlugin.extractEvents 方法
  • 现在单独看下这里面的细节过程,即如何去生产这个事件对象的过程

2 )源码

定位到 packages/events/EventPluginHub.js#L172

js 复制代码
function extractEvents(
  topLevelType: TopLevelType,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: EventTarget,
): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
  let events = null;
  for (let i = 0; i < plugins.length; i++) {
    // Not every plugin in the ordering may be loaded at runtime.
    const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
    if (possiblePlugin) {
      // 这里要去调用 每个 plugin 的 extractEvents 方法
      const extractedEvents = possiblePlugin.extractEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
      );
      if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
      }
    }
  }
  return events;
}

注意这里的 possiblePlugin.extractEvents 我们专门专注下 changeEvent 的这个方法

定位到 packages/react-dom/src/events/ChangeEventPlugin.js#L263

js 复制代码
const ChangeEventPlugin = {
  eventTypes: eventTypes,

  _isInputEventSupported: isInputEventSupported,
  // 注意这里
  extractEvents: function(
    topLevelType,
    targetInst,
    nativeEvent,
    nativeEventTarget,
  ) {
    // 在这个方法里面,拿到了 targetNode
    // 因为这边传进来的呢是一个发布对象, 所以要通过这种方法拿到它的 node
    const targetNode = targetInst ? getNodeFromInstance(targetInst) : window;

    // 然后,要经过一系列的判断,主要去赋值了不同的 getTargetInstFunc
    let getTargetInstFunc, handleEventFunc;
    // 如果有返回这个instance,那么我们就可以去创建这个event了,这是什么意思呢?
    // 就是说我们这个 event plugin, 在所有的事件触发的过程当中,这个plugin都会被循环调用的
    // 它是没有通过事件名称来调用不同的plugin这么一个设置的
    // 所以这个判断是要放在每个pluggin里面自己去做。就是说根据这次触发的具体事件是什么?
    // 来判断我们要不要为它创建一个event,因为每个plugin在每次事件触发都会被调用
    // 如果我们都生成事件,那么明显是不对的,肯定要对自己这个 plugin 关心的事件来去为它生成这个事件
    if (shouldUseChangeEvent(targetNode)) {
      getTargetInstFunc = getTargetInstForChangeEvent;
    } else if (isTextInputElement(targetNode)) {
      if (isInputEventSupported) {
        getTargetInstFunc = getTargetInstForInputOrChangeEvent;
      } else {
        // polyfill 的这些先忽略
        getTargetInstFunc = getTargetInstForInputEventPolyfill;
        handleEventFunc = handleEventsForInputEventPolyfill;
      }
    } else if (shouldUseClickEvent(targetNode)) {
      getTargetInstFunc = getTargetInstForClickEvent;
    }

    // 基于类型,得到了最终的处理函数
    if (getTargetInstFunc) {
      const inst = getTargetInstFunc(topLevelType, targetInst);
      if (inst) {
        // 创建 event
        const event = createAndAccumulateChangeEvent(
          inst,
          nativeEvent,
          nativeEventTarget,
        );
        return event;
      }
    }

    if (handleEventFunc) {
      handleEventFunc(topLevelType, targetNode, targetInst);
    }

    // When blurring, set the value attribute for number inputs
    if (topLevelType === TOP_BLUR) {
      handleControlledInputBlur(targetNode);
    }
  },
};
  • 进入 shouldUseChangeEvent

    js 复制代码
    function shouldUseChangeEvent(elem) {
      const nodeName = elem.nodeName && elem.nodeName.toLowerCase();
      return (
        nodeName === 'select' || (nodeName === 'input' && elem.type === 'file')
      );
    }
    • 这个判断其实就是主要来判断一下我们这个节点上面是否有changeevent

    • 并且是否应该用 change event 来进行一个触发

    • 因为react当中的 onchange 事件,其实它是封装了各种不同的事件的

    • 比如说对于像我们输入文本的 input type='text' 的一个情况

    • 正常来讲,应该绑定的是input事件,而不是change事件

    • 因为change事件在有些浏览器里面要等到这个输入框,blur的时候,才会真正触发这个change事件

    • 对于input事件是我们每次有输入变化的时候,都会触发的这个事件

    • 所以对于 select 还有 input type='file', 它们的change是非常明显的,就是等到它们有内容变化的时候就会触发

    • 因为file是我们选择了一个文件之后,它就会触发change事件

    • 而select我们选择了某一个 option 之后,它也会触发这个change事件

    • 所以对于这种节点,我们可以直接使用onchange来进行一个绑定

    • 这时候, getTargetInstFunc = getTargetInstForChangeEvent; 进入 getTargetInstForChangeEvent

      js 复制代码
      function getTargetInstForChangeEvent(topLevelType, targetInst) {
        // TOP_CHANGE 就是 change
        if (topLevelType === TOP_CHANGE) {
          return targetInst;
        }
      }
      
      // 注意 另外的文件中
      // packages/react-dom/src/events/DOMTopLevelEventTypes.js#L41
      export const TOP_CHANGE = unsafeCastStringToDOMTopLevelType('change');
      
      // packages/events/TopLevelEventTypes.js#L27
      export function unsafeCastStringToDOMTopLevelType(
        topLevelType: string,
      ): DOMTopLevelEventType {
        return topLevelType;
      }
  • 进入 isTextInputElement

    js 复制代码
    const supportedInputTypes: {[key: string]: true | void} = {
      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: ?HTMLElement): boolean {
      // 获取 nodeName
      const nodeName = elem && elem.nodeName && elem.nodeName.toLowerCase();
    
      // 判断在 input 的时候,是否符合支持的type类型
      if (nodeName === 'input') {
        return !!supportedInputTypes[((elem: any): HTMLInputElement).type];
      }
    
      if (nodeName === 'textarea') {
        return true;
      }
    
      return false;
    }
    • 这里就是一个 boolean 类型的函数进行类型判断的
  • 进入 getTargetInstForInputOrChangeEvent

    js 复制代码
    // packages/react-dom/src/events/ChangeEventPlugin.js#L229
    function getTargetInstForInputOrChangeEvent(topLevelType, targetInst) {
      if (topLevelType === TOP_INPUT || topLevelType === TOP_CHANGE) {
        return getInstIfValueChanged(targetInst);
      }
    }
    
    // packages/react-dom/src/events/ChangeEventPlugin.js#L106
    function getInstIfValueChanged(targetInst) {
      const targetNode = getNodeFromInstance(targetInst);
      if (inputValueTracking.updateValueIfChanged(targetNode)) {
        return targetInst;
      }
    }
    
    // packages/react-dom/src/client/ReactDOMComponentTree.js#L69
    export function getNodeFromInstance(inst) {
      if (inst.tag === HostComponent || inst.tag === HostText) {
        // In Fiber this, is just the state node right now. We assume it will be
        // a host component or host text.
        return inst.stateNode;
      }
    
      // Without this first invariant, passing a non-DOM-component triggers the next
      // invariant for a missing parent, which is super confusing.
      invariant(false, 'getNodeFromInstance: Invalid argument.');
    }
  • 进入 shouldUseClickEvent

    js 复制代码
    /**
     * SECTION: handle `click` event
     */
     // checkbox 和 radio 的特殊处理
    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')
      );
    }
  • 进入 getTargetInstForClickEvent

    js 复制代码
    function getTargetInstForClickEvent(topLevelType, targetInst) {
      if (topLevelType === TOP_CLICK) {
        return getInstIfValueChanged(targetInst);
      }
    }
  • 进入 createAndAccumulateChangeEvent 创建事件对象

    js 复制代码
    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;
    }
    • 这个函数就是具体生成这个事件的一个过程

      • 可以看到这个事件,接收了一个 inst,然后传入了 nativeEvent,并且再传入 target
      • 然后,通过 SyntheticEvent.getPooled,就是说在react当中所有的事件对象是通过一个 pool 来进行一个存储的
      • 比如说我们为所有的event创建了十个event对象
      • 每一次有新的一个event进来的时候,从这个pool里面拿出一个设置一些事件以及对应的一些值之后
      • 去触发每一个事件的监听方法,然后去使用这个 event 对象
      • 这个 event 对象使用完了之后,又会归还到这个 pool 里面
      • 也就是一个 能够减少 对象声明 以及 对象回收 的一个性能开销
      • 然后拿到了这个 event 之后,给它设置了 type 是 change
      • 之后执行两个函数 enqueueStateRestoreaccumulateTwoPhaseDispatches
    • 进入 SyntheticEvent.getPooled

      js 复制代码
      // packages/events/SyntheticEvent.js#L335
      function addEventPoolingTo(EventConstructor) {
        EventConstructor.eventPool = [];
        EventConstructor.getPooled = getPooledEvent; // 注意这里
        EventConstructor.release = releasePooledEvent;
      }
      • 进入 getPooledEvent

        js 复制代码
        // packages/events/SyntheticEvent.js#L300
        function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
          const EventConstructor = this;
          // 存在poll
          if (EventConstructor.eventPool.length) {
            const instance = EventConstructor.eventPool.pop();
            EventConstructor.call(
              instance,
              dispatchConfig,
              targetInst,
              nativeEvent,
              nativeInst,
            );
            return instance;
          }
          // pool 里面没有,则创建一个新的
          return new EventConstructor(
            dispatchConfig,
            targetInst,
            nativeEvent,
            nativeInst,
          );
        }
        • 关于 EventConstructor
          • 首先在 packages/events/SyntheticEvent.js#L62 中的 SyntheticEvent 构造方法
          • 要理解这个过程, 首先在这个js里面先声明了一个叫做 SyntheticEvent 这么一个方法
          • 这个方法它是一个constructor 方法, 在这个方法里面去声明事件相关的各种属性
          • 重点关注它的事件的一个触发的过程以及生产的过程,所以只关心它的 pool 的处理过程
        • 这里的 this 就是 SyntheticEvent
          • 在这个构造方法的原型链上也有一大堆的东西,对事件对象进行一个封装和扩展
        • 注意这里的 EventConstructor.callnew EventConstructor 都达到同一个目的
    • 进入 enqueueStateRestore

      js 复制代码
      export function enqueueStateRestore(target: EventTarget): void {
        // 判断了这个 restoreTarget 公共变量是否存在
        if (restoreTarget) {
          if (restoreQueue) {
            restoreQueue.push(target);
          } else {
            restoreQueue = [target];
          }
        // restoreTarget 不存在,则对其进行赋值
        } else {
          restoreTarget = target;
        }
      }
      • 作用是处理,如果setState之后,这个 state 对应的 input 的 value 是不一样的
      • 要把这个值进行一个回滚
    • 进入 accumulateTwoPhaseDispatches 这个方法才是真正要去从每个节点上面去获取它的 listener 的一个过程

      js 复制代码
      // packages/events/EventPropagators.js#L115
      export function accumulateTwoPhaseDispatches(events) {
        // 其实就是对 events 这个数组里面,它的每一个节点去调用这个方法
        forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
      }
      
      function accumulateTwoPhaseDispatchesSingle(event) {
        // 存在 phasedRegistrationNames 则调用 traverseTwoPhase
        if (event && event.dispatchConfig.phasedRegistrationNames) {
          traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
        }
      }
      
      // 在 event 对象上 插入 listener 的过程
      function accumulateDirectionalDispatches(inst, phase, event) {
        // 忽略
        if (__DEV__) {
          warningWithoutStack(inst, 'Dispatching inst must not be null');
        }
        // 获取 listener
        const listener = listenerAtPhase(inst, event, phase);
        if (listener) {
          // 注意这里 event._dispatchListeners 和 下面的 event._dispatchInstances 保持两者一一对应的关系
          event._dispatchListeners = accumulateInto(
            event._dispatchListeners,
            listener,
          );
          event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
        }
      }
      
      function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
        const registrationName =
          event.dispatchConfig.phasedRegistrationNames[propagationPhase];
        return getListener(inst, registrationName);
      }
      
      // packages/events/EventPluginHub.js#L126
      export function getListener(inst: Fiber, registrationName: string) {
        let listener;
      
        // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
        // live here; needs to be moved to a better place soon
        const stateNode = inst.stateNode;
        if (!stateNode) {
          // Work in progress (ex: onload events in incremental mode).
          return null;
        }
        const props = getFiberCurrentPropsFromNode(stateNode); // 从 dom tree上获取 props
        if (!props) {
          // Work in progress.
          return null;
        }
        listener = props[registrationName];
        if (shouldPreventMouseEvent(registrationName, inst.type, props)) {
          return null;
        }
        invariant(
          !listener || typeof listener === 'function',
          'Expected `%s` listener to be a function, instead got a value of `%s` type.',
          registrationName,
          typeof listener,
        );
        return listener;
      }
      
      // packages/shared/ReactTreeTraversal.js#L86
      export function traverseTwoPhase(inst, fn, arg) {
        const path = [];
        // 找到所有上层节点,并存入 path
        while (inst) {
          path.push(inst);
          inst = getParent(inst);
        }
        // 下面是核心,执行两个阶段的回调,捕获和冒泡
        let i;
        for (i = path.length; i-- > 0; ) { // 注意这个 i 的顺序
          // path[i]:节点, arg:event
          fn(path[i], 'captured', arg); // captured 是从 window 向下触发的
        }
        for (i = 0; i < path.length; i++) { // 注意这个 i 的顺序
          fn(path[i], 'bubbled', arg); // bubbled 是从下向 window 方向的
        }
        // 基于上面两个 循环
        // 这样的话,就不需要在 event 对象上面单独维护 capture 的这个事件的它的一个数组
        // 还有 bubble 的事件的一个速度,只需要放在同一个数组里面,然后按照这个数组的顺序去触发就可以了
      }
      // packages/shared/ReactTreeTraversal.js#L10
      function getParent(inst) {
        do {
          inst = inst.return;
          // TODO: If this is a HostRoot we might want to bail out.
          // That is depending on if we want nested subtrees (layers) to bubble
          // events to their parent. We could also go through parentNode on the
          // host node but that wouldn't work for React Native and doesn't let us
          // do the portal feature.
        } while (inst && inst.tag !== HostComponent);
        if (inst) {
          return inst; // 返回的 inst 是一个 HostComponent
        }
        return null;
      }
  • 以上,生产 event 对象,然后去挂载它的事件,这个过程是非常的复杂的

  • react 团队把整个事件系统去重新抽象的这么一个过程,而且设计的超级复杂

  • 这一套东西只是非常适合react,在其他框架要使用这类event库,会有很大的成本

  • 目前为止,通过 ChangeEventPlugin 来了解了整个 event 对象的处理过程

  • 后续其他的类似事件的处理逻辑到后面都是一样的

  • 但每一个 plugin 或多或少有一些自己的一些区别,这里不再赘述

相关推荐
持久的棒棒君12 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979123 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
winkee3 小时前
在 git commit 中使用 gpg key 进行签名
架构·前端框架·代码规范
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
无知的小菜鸡3 小时前
路由:ReactRouter
react.js
寻找09之夏4 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js