React16源码: React中event事件监听绑定的源码实现

news/2024/7/15 18:27:39 标签: react.js, 前端, 前端框架

event事件监听


1 )概述

  • 在 react-dom 代码初始化的时候,去注入了平台相关的事件插件
  • 接下去在react的更新过程绑定了事件的操作,在执行到 completeWork 的时候
  • 对于 HostComponent 会一开始就先去执行了 finalizeInitialChildren 这个方法
  • 位置在 packages/react-reconciler/src/ReactFiberCompleteWork.js#L642

2 )源码

定位到 packages/react-dom/src/client/ReactDOMHostConfig.js#L212

找到 finalizeInitialChildren

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}

定位到 packages/react-dom/src/client/ReactDOMComponent.js#L447

找到 setInitialProperties

export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document,
): void {
  const isCustomComponentTag = isCustomComponent(tag, rawProps);
  if (__DEV__) {
    validatePropertiesInDevelopment(tag, rawProps);
    if (
      isCustomComponentTag &&
      !didWarnShadyDOM &&
      (domElement: any).shadyRoot
    ) {
      warning(
        false,
        '%s is using shady DOM. Using shady DOM with React can ' +
          'cause things to break subtly.',
        getCurrentFiberOwnerNameInDevOrNull() || 'A component',
      );
      didWarnShadyDOM = true;
    }
  }

  // TODO: Make sure that we check isMounted before firing any of these events.
  let props: Object;
  switch (tag) {
    case 'iframe':
    case 'object':
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'video':
    case 'audio':
      // Create listener for each media event
      for (let i = 0; i < mediaEventTypes.length; i++) {
        trapBubbledEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    case 'source':
      trapBubbledEvent(TOP_ERROR, domElement);
      props = rawProps;
      break;
    case 'img':
    case 'image':
    case 'link':
      trapBubbledEvent(TOP_ERROR, domElement);
      trapBubbledEvent(TOP_LOAD, domElement);
      props = rawProps;
      break;
    case 'form':
      trapBubbledEvent(TOP_RESET, domElement);
      trapBubbledEvent(TOP_SUBMIT, domElement);
      props = rawProps;
      break;
    case 'details':
      trapBubbledEvent(TOP_TOGGLE, domElement);
      props = rawProps;
      break;
    case 'input':
      ReactDOMInput.initWrapperState(domElement, rawProps);
      props = ReactDOMInput.getHostProps(domElement, rawProps);
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'option':
      ReactDOMOption.validateProps(domElement, rawProps);
      props = ReactDOMOption.getHostProps(domElement, rawProps);
      break;
    case 'select':
      ReactDOMSelect.initWrapperState(domElement, rawProps);
      props = ReactDOMSelect.getHostProps(domElement, rawProps);
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    case 'textarea':
      ReactDOMTextarea.initWrapperState(domElement, rawProps);
      props = ReactDOMTextarea.getHostProps(domElement, rawProps);
      trapBubbledEvent(TOP_INVALID, domElement);
      // For controlled components we always need to ensure we're listening
      // to onChange. Even if there is no listener.
      ensureListeningTo(rootContainerElement, 'onChange');
      break;
    default:
      props = rawProps;
  }

  assertValidProps(tag, props);

  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );

  switch (tag) {
    case 'input':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      inputValueTracking.track((domElement: any));
      ReactDOMInput.postMountWrapper(domElement, rawProps, false);
      break;
    case 'textarea':
      // TODO: Make sure we check if this is still unmounted or do any clean
      // up necessary since we never stop tracking anymore.
      inputValueTracking.track((domElement: any));
      ReactDOMTextarea.postMountWrapper(domElement, rawProps);
      break;
    case 'option':
      ReactDOMOption.postMountWrapper(domElement, rawProps);
      break;
    case 'select':
      ReactDOMSelect.postMountWrapper(domElement, rawProps);
      break;
    default:
      if (typeof props.onClick === 'function') {
        // TODO: This cast may not be sound for SVG, MathML or custom elements.
        trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
      }
      break;
  }
}
  • 对于 iframe, object, video, audio, source 这些多媒体节点的初始化绑定
  • 是通过 trapBubbledEvent 来实现的
  • 后续执行到 setInitialDOMProperties, 在这个方法内部
    function setInitialDOMProperties(
      tag: string,
      domElement: Element,
      rootContainerElement: Element | Document,
      nextProps: Object,
      isCustomComponentTag: boolean,
    ): void {
      for (const propKey in nextProps) {
        if (!nextProps.hasOwnProperty(propKey)) {
          continue;
        }
        const nextProp = nextProps[propKey];
        if (propKey === STYLE) {
          // ... 跳过很多代码
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
          // ... 跳过很多代码
        } else if (propKey === AUTOFOCUS) {
          // We polyfill it separately on the client during commit.
          // We could have excluded it in the property list instead of
          // adding a special case here, but then it wouldn't be emitted
          // on server rendering (but we *do* want to emit it in SSR).
          // 注意这里, propKey 是 dom 节点内的 props的配置,如果这个配置在 registrationNameModules 这里
          // registrationNameModules 是通过每一个插件里面每一个 eventTypes 里面
          // 它对应的有 phasedRegistrationNames 的情况下,比如说 onChange, onChangeCapture, 它都是作为它的一个key而存在的
          // 也就是说我们如果在这个 props 上面写了 onChange onClick 这些事件相关的props的话
          // 就会符合这个条件的判断,符合这个条件判断之后, 它会调用一个方法叫做 ensureListeningTo
        } else if (registrationNameModules.hasOwnProperty(propKey)) {
          if (nextProp != null) {
            if (__DEV__ && typeof nextProp !== 'function') {
              warnForInvalidEventListener(propKey, nextProp);
            }
            ensureListeningTo(rootContainerElement, propKey); // rootContainerElement 是 fiberRoot 对应的 container 
          }
        } else if (nextProp != null) {
         // ... 跳过很多代码
        }
      }
    }
    
    • 进入 ensureListeningTo
      function ensureListeningTo(rootContainerElement, registrationName) {
        const isDocumentOrFragment =
          rootContainerElement.nodeType === DOCUMENT_NODE ||
          rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
        // 如果它是一个document或者fragment,那么它就等于 rootContainerElement
        // 如果它不是,它就等于它的 rootContainerElement.ownerDocument
        // 这个是用来最终要去把事件绑定在哪个地方的
        // 可以确定的是在 react 当中大部分可冒泡的事件都是通过事件代理的形式来进行一个绑定的
        // 也就是说,不是每一个节点都会绑定自己的事件
        // 因为每个节点绑定自己的事件,肯定是性能比较低下的一个操作,而且有可能会导致内存溢出这种情况
        const doc = isDocumentOrFragment
          ? rootContainerElement
          : rootContainerElement.ownerDocument;
        // 调用这个方法
        listenTo(registrationName, doc);
      }
      
      • 进入 listenTo
        // packages/react-dom/src/events/ReactBrowserEventEmitter.js#L126
        export function listenTo(
          registrationName: string,
          mountAt: Document | Element,
        ) {
          // 注意这里
          const isListening = getListeningForDocument(mountAt);
          const dependencies = registrationNameDependencies[registrationName];
        
          // 遍历依赖
          for (let i = 0; i < dependencies.length; i++) {
            const dependency = dependencies[i];
            // 没有这些依赖,则对 dependency 进行事件监听处理
            if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
              switch (dependency) {
                case TOP_SCROLL:
                  // 这个方法监听的是 捕获阶段的事件
                  trapCapturedEvent(TOP_SCROLL, mountAt);
                  break;
                case TOP_FOCUS:
                case TOP_BLUR:
                  trapCapturedEvent(TOP_FOCUS, mountAt);
                  trapCapturedEvent(TOP_BLUR, mountAt);
                  // We set the flag for a single dependency later in this function,
                  // but this ensures we mark both as attached rather than just one.
                  isListening[TOP_BLUR] = true;
                  isListening[TOP_FOCUS] = true;
                  break;
                case TOP_CANCEL:
                case TOP_CLOSE:
                  if (isEventSupported(getRawEventName(dependency))) {
                    trapCapturedEvent(dependency, mountAt);
                  }
                  break;
                case TOP_INVALID:
                case TOP_SUBMIT:
                case TOP_RESET:
                  // We listen to them on the target DOM elements.
                  // Some of them bubble so we don't want them to fire twice.
                  break;
                // 对于其他大部分的事件处理 用冒泡处理
                default:
                  // By default, listen on the top level to all non-media events.
                  // Media events don't bubble so adding the listener wouldn't do anything.
                  const isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1; 
                  // 注意这里排除了 mediaEventTypes,因为一开始就已经对一些 媒体事件处理了
                  if (!isMediaEvent) {
                    trapBubbledEvent(dependency, mountAt); // 这是对常规事件的处理 冒泡
                  }
                  break;
              }
              isListening[dependency] = true;
            }
          }
        }
        
        • 进入 getListeningForDocument

          const alreadyListeningTo = {};
          let reactTopListenersCounter = 0;
          // 这个属性就是用来挂载 container 节点上面去记录这个节点监听了哪些事件的
          // 用这种方式判断是因为 可能不存在这个属性,如果没有,则需要初始化属性
          const topListenersIDKey = '_reactListenersID' + ('' + Math.random()).slice(2);
          function getListeningForDocument(mountAt: any) {
            // In IE8, `mountAt` is a host object and doesn't have `hasOwnProperty`
            // directly.
            if (!Object.prototype.hasOwnProperty.call(mountAt, topListenersIDKey)) {
              mountAt[topListenersIDKey] = reactTopListenersCounter++; // 这里初始化属性
              alreadyListeningTo[mountAt[topListenersIDKey]] = {};
            }
            // 那如果已经有了,我们就返回这个对象, 用来记录这个dom节点它是否监听了哪些事件的
            return alreadyListeningTo[mountAt[topListenersIDKey]];
          }
          
        • 对于 mediaEventTypes 和媒体相关的事件

          // packages/react-dom/src/events/DOMTopLevelEventTypes.js#L155
          export const mediaEventTypes = [
            TOP_ABORT,
            TOP_CAN_PLAY,
            TOP_CAN_PLAY_THROUGH,
            TOP_DURATION_CHANGE,
            TOP_EMPTIED,
            TOP_ENCRYPTED,
            TOP_ENDED,
            TOP_ERROR,
            TOP_LOADED_DATA,
            TOP_LOADED_METADATA,
            TOP_LOAD_START,
            TOP_PAUSE,
            TOP_PLAY,
            TOP_PLAYING,
            TOP_PROGRESS,
            TOP_RATE_CHANGE,
            TOP_SEEKED,
            TOP_SEEKING,
            TOP_STALLED,
            TOP_SUSPEND,
            TOP_TIME_UPDATE,
            TOP_VOLUME_CHANGE,
            TOP_WAITING,
          ];
          
        • 进入 trapCapturedEvent

          export function trapCapturedEvent(
            topLevelType: DOMTopLevelEventType,
            element: Document | Element,
          ) {
            if (!element) {
              return null;
            }
            // 注意这里,根据是否是 Interactive 类型的事件,调用的不同的回调,最终赋值给 dispatch
            const dispatch = isInteractiveTopLevelEventType(topLevelType)
              ? dispatchInteractiveEvent
              : dispatchEvent;
          
            addEventCaptureListener(
              element,
              getRawEventName(topLevelType),
              // Check if interactive and wrap in interactiveUpdates
              dispatch.bind(null, topLevelType),
            );
          }
          
          • 进入 isInteractiveTopLevelEventType
            const SimpleEventPlugin: PluginModule<MouseEvent> & {
              isInteractiveTopLevelEventType: (topLevelType: TopLevelType) => boolean,
            } = {
              eventTypes: eventTypes,
              // 注意这里的 topLevelEventsToDispatchConfig 一开始是一个空的对象
              // 在调用 addEventTypeNameToConfig 时候加入的
              // 这个方法是检测 
              isInteractiveTopLevelEventType(topLevelType: TopLevelType): boolean {
                const config = topLevelEventsToDispatchConfig[topLevelType];
                return config !== undefined && config.isInteractive === true;
              },
              // ... 跳过其他
            }
            
            • 进入 addEventTypeNameToConfig
              function addEventTypeNameToConfig(
                [topEvent, event]: EventTuple,
                isInteractive: boolean,
              ) {
                const capitalizedEvent = event[0].toUpperCase() + event.slice(1);
                const onEvent = 'on' + capitalizedEvent;
              
                // 注意这个数据结构
                const type = {
                  phasedRegistrationNames: {
                    bubbled: onEvent,
                    captured: onEvent + 'Capture',
                  },
                  dependencies: [topEvent],
                  isInteractive, // 注意这里的标识
                };
                eventTypes[event] = type;
                topLevelEventsToDispatchConfig[topEvent] = type; // 这里进行注入
              }
              
              • 关于这里的 isInteractive 标识的来源
                interactiveEventTypeNames.forEach(eventTuple => {
                  addEventTypeNameToConfig(eventTuple, true);
                });
                nonInteractiveEventTypeNames.forEach(eventTuple => {
                  addEventTypeNameToConfig(eventTuple, false);
                });
                
                • 其中 interactiveEventTypeNames
                  • 参考 packages/react-dom/src/events/SimpleEventPlugin.js#L59
                • 其中 nonInteractiveEventTypeNames
                  • 参考 packages/react-dom/src/events/SimpleEventPlugin.js#L95
                • 上面两个数组对应 dom 原生的事件, 它们的区别是什么呢
                  • 这些事件去调用了设置的事件回调之后,里面如果有 setState
                  • 那么创建了update去计算的 expirationTime 会有 interactive 和 nonInteractive 的区分
                  • 它们的区别在 expirationTime,interactive的会比较的小
                  • 也就是说它的优先级会比较的高,它需要优先被执行
                  • 因为它是一个用户交互相关的事件,希望是用户比如说点了一个按钮之后
                  • 立马可以得到反馈, 因为它需要被优先执行的
          • 进入 addEventCaptureListener
            // 两者区别是第三个参数,bubble 是 false, capture 是 true
            export function addEventBubbleListener(
              element: Document | Element,
              eventType: string,
              listener: Function,
            ): void {
              element.addEventListener(eventType, listener, false); 
            }
            
            // 注意这里
            export function addEventCaptureListener(
              element: Document | Element,
              eventType: string,
              listener: Function,
            ): void {
              element.addEventListener(eventType, listener, true); // 主要是绑定 dom 原生事件
            }
            
        • 同样对于 trapBubbledEvent 也同上类似,这里不再赘述


http://www.niftyadmin.cn/n/5359754.html

相关文章

【已解决】QT如何加载qss文件

翻阅我的博客&#xff0c;发现没有写这方面文章&#xff0c;qt如何加载qss文件。今天补上一篇关于qt如何加载qss文件。写完这篇博客&#xff0c;未来有相关问题&#xff0c;直接可以从博客里去搜。 解决方案 在项目里写完qss文件&#xff0c;在main文件里只需要追加这句话。 …

Android矩阵setRectToRect裁剪Bitmap原图Matrix放大,mapRect标记中心区域,Kotlin

Android矩阵setRectToRect裁剪Bitmap原图Matrix放大&#xff0c;mapRect标记中心区域&#xff0c;Kotlin import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Matrix impo…

Leetcode—2950. 可整除子串的数量【中等】Plus(前缀和题型)

2024每日刷题&#xff08;一零八&#xff09; Leetcode—2950. 可整除子串的数量 算法思想 让 f ( c ) d , 其中 d 1 , 2 , . . . , 9 f(c) d, 其中d 1, 2, ..., 9 f(c)d,其中d1,2,...,9. // f(c1) f(c2) ... f(ck) / k avg // > f(c1) f(c2) ... f(ck) - …

ImportError: DLL load failed while importing _network: 找不到指定的模块。

ImportError: DLL load failed while importing _network: 找不到指定的模块。 具体问题如下所示&#xff0c;在python导入cartopy时出现的报错&#xff1a; import cartopy.crs as crsimport cartopy.crs # noqa: E402 module-level importsfrom pyproj import Transforme…

【计算机二级考试C语言】C递归

目录 C 递归 数的阶乘 实例 斐波那契数列 实例 C 递归 递归指的是在函数的定义中使用函数自身的方法。 举个例子&#xff1a; 从前有座山&#xff0c;山里有座庙&#xff0c;庙里有个老和尚&#xff0c;正在给小和尚讲故事呢&#xff01;故事是什么呢&#xff1f;"从…

Python 字段进行分组和聚合操作

基础 要对多个字段进行分组和聚合操作&#xff0c;你可以在 groupby 方法中传入多个字段的名称&#xff0c;然后使用 agg 方法对多个字段进行不同的聚合操作。 以下是一个示例&#xff0c;演示了如何对多个字段进行分组和聚合操作&#xff1a; import pandas as pd# 创建一个…

Maya------布尔 圆形圆角组件

17. maya常用命令7.布尔 圆形圆角组件_哔哩哔哩_bilibili 选中一个模型&#xff0c;再按shift加选另外一个模型 圆形圆角命令

Jasperreport 生成 PDF之省纸模式

省纸模式顾名思义就是节省纸张&#xff0c;使用 Jasper 去生成 PDF 的时候如果进行分组打印的时候&#xff0c;一页 A4 纸只会打印一组数据。这种情况下&#xff0c;如果每组数据特别少&#xff0c;只有几行&#xff0c;一页 A4 纸张根本用不了&#xff0c;就会另起一页继续打印…