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

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 也同上类似,这里不再赘述

相关推荐

最近更新

  1. TCP协议是安全的吗?

    2024-02-02 07:32:06       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-02 07:32:06       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-02 07:32:06       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-02 07:32:06       20 阅读

热门阅读

  1. ASP.NET Core WebAPI_解决跨域问题(前端后端)

    2024-02-02 07:32:06       31 阅读
  2. AI 代码生成

    2024-02-02 07:32:06       36 阅读
  3. springboot接入influxdb

    2024-02-02 07:32:06       35 阅读
  4. Camille-学习笔记-SQL语法与数据库

    2024-02-02 07:32:06       25 阅读
  5. sqlite不会自动缩减问题

    2024-02-02 07:32:06       31 阅读
  6. 服务器渲染(SSR)-前端框架

    2024-02-02 07:32:06       25 阅读
  7. 前端框架和组件库的区别与联系

    2024-02-02 07:32:06       33 阅读
  8. 【Leetcode热题100】

    2024-02-02 07:32:06       35 阅读