React16源码: React中event事件触发的源码实现

event 事件触发过程

1 )概述

  • 在之前事件绑定时,绑定的是两个方法
    • 一个是 dispatchInteractiveEvent
    • 另外一个 dispatchEvent
  • 其实它们调用的方法都是差不多的,一开始会有一点小的区别

2 )源码

定位到 packages/react-dom/src/events/ReactDOMEventListener.js#L165

进入 trapCapturedEvent

js 复制代码
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
    // 这边的topleveltype呢,是我们在进行 dom 的事件绑定的时候已经通过 bind 给它绑定好了
    // 在绑定事件的时候,就已经确定了一个值, 比如说是onChange这类的toplevel的事件名称
    dispatch.bind(null, topLevelType),
  );
}

2.1 先看 dispatchInteractiveEvent

定位到 packages/react-dom/src/events/ReactDOMEventListener.js#L184

js 复制代码
// packages/react-dom/src/events/ReactDOMEventListener.js#L184
// nativeEvent,就是我们事件触发的时候,我们的domm的事件体系会给我们一个event对象
// 可以通过它来进入默认行为之类的这么一个事件对象
function dispatchInteractiveEvent(topLevelType, nativeEvent) {
  interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}

// packages/events/ReactGenericBatching.js#L55
export function interactiveUpdates(fn, a, b) {
  return _interactiveUpdatesImpl(fn, a, b);
}

// packages/events/ReactGenericBatching.js#L23
let _interactiveUpdatesImpl = function(fn, a, b) {
  return fn(a, b);
};
  • 可见 dispatchInteractiveEvent 最终还是要调用 dispatchEvent, 只是多包了一层

2.2 进入 dispatchEvent

定位到 packages/react-dom/src/events/ReactDOMEventListener.js#L188

js 复制代码
export function dispatchEvent(
  topLevelType: DOMTopLevelEventType,
  nativeEvent: AnyNativeEvent,
) {
  if (!_enabled) {
    return;
  }

  // 注意这里
  const nativeEventTarget = getEventTarget(nativeEvent);
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);
  // 存在 符合条件的,并且没有被挂载
  if (
    targetInst !== null &&
    typeof targetInst.tag === 'number' &&
    !isFiberMounted(targetInst)
  ) {
    // If we get an event (ex: img onload) before committing that
    // component's mount, ignore it for now (that is, treat it as if it was an
    // event on a non-React tree). We might also consider queueing events and
    // dispatching them after the mount.
    targetInst = null; // 这个 target 置空
  }

  // 这里只是一个对象,用于携带信息
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
  );

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}
  • 首先它要获取 nativeEventTarget,这个 target 就是我们 event 对象上面的 target

  • 它是出于对各种系统的一个兼容调用的一个方法来进行一个 Polyfill

  • 进入这个 getEventTarget 方法

    js 复制代码
    import {TEXT_NODE} from '../shared/HTMLNodeType';
    
    /**
     * Gets the target node from a native browser event by accounting for
     * inconsistencies in browser DOM APIs.
     *
     * @param {object} nativeEvent Native browser event.
     * @return {DOMEventTarget} Target node.
     */
    function getEventTarget(nativeEvent) {
      // 这个是对 IE9 的兼容
      // Fallback to nativeEvent.srcElement for IE9
      // https://github.com/facebook/react/issues/12506
      let target = nativeEvent.target || nativeEvent.srcElement || window;
    
      // Normalize SVG <use> element events #4963
      if (target.correspondingUseElement) {
        target = target.correspondingUseElement;
      }
    
      // Safari may fire events on text nodes (Node.TEXT_NODE is 3).
      // @see http://www.quirksmode.org/js/events_properties.html
      return target.nodeType === TEXT_NODE ? target.parentNode : target;
    }
    • 这个方法主要是为了兼容浏览器API获取 target 对象, 最终根据是否是 TEXT_NODE 返回 target.parentNode 或 target
  • 进入 getClosestInstanceFromNode

    js 复制代码
    export function getClosestInstanceFromNode(node) {
      // 存在,直接 return
      if (node[internalInstanceKey]) {
        return node[internalInstanceKey]; // 这个就是 初始化dom节点的时候,在node上插入这么一个key, 来指定对应的 fiber 对象
      }
    
      // 不存在,一直找 parentNode
      while (!node[internalInstanceKey]) {
        if (node.parentNode) {
          node = node.parentNode;
        } else {
          // Top of the tree. This node must not be part of a React tree (or is
          // unmounted, potentially).
          return null;
        }
      }
      // 这个 inst 就是一个 fiber 对象, 找到是 HostComponent 或 HostText 类型的
      let inst = node[internalInstanceKey];
      if (inst.tag === HostComponent || inst.tag === HostText) {
        // In Fiber, this will always be the deepest root.
        return inst;
      }
    
      return null;
    }
  • 进入 isFiberMounted

    js 复制代码
    const MOUNTING = 1;
    const MOUNTED = 2;
    const UNMOUNTED = 3;
    
    function isFiberMountedImpl(fiber: Fiber): number {
      let node = fiber;
      // 不存在,说明是即将插入或没有插入的节点
      if (!fiber.alternate) {
        // If there is no alternate, this might be a new tree that isn't inserted
        // yet. If it is, then it will have a pending insertion effect on it.
        // 这种是即将要插入的
        if ((node.effectTag & Placement) !== NoEffect) {
          return MOUNTING;
        }
        // 如果当前不是,向上找父节点,也是即将插入的
        while (node.return) {
          node = node.return;
          if ((node.effectTag & Placement) !== NoEffect) {
            return MOUNTING;
          }
        }
      } else {
        // 存在 alternate, 继续向上找
        while (node.return) {
          node = node.return;
        }
      }
      // 如果上级存在,并且是 HostRoot 说明已经被挂载了
      if (node.tag === HostRoot) {
        // TODO: Check if this was a nested HostRoot when used with
        // renderContainerIntoSubtree.
        return MOUNTED;
      }
      // If we didn't hit the root, that means that we're in an disconnected tree
      // that has been unmounted.
      // 其他情况都是未挂载的
      return UNMOUNTED;
    }
    
    export function isFiberMounted(fiber: Fiber): boolean {
      return isFiberMountedImpl(fiber) === MOUNTED;
    }
  • 进入 getTopLevelCallbackBookKeeping

    js 复制代码
    // packages/react-dom/src/events/ReactDOMEventListener.js#L50
    const CALLBACK_BOOKKEEPING_POOL_SIZE = 10;
    const callbackBookkeepingPool = [];
    
    // Used to store ancestor hierarchy in top level callback
    function getTopLevelCallbackBookKeeping(
      topLevelType,
      nativeEvent,
      targetInst,
    ): {
      topLevelType: ?DOMTopLevelEventType,
      nativeEvent: ?AnyNativeEvent,
      targetInst: Fiber | null,
      ancestors: Array<Fiber>,
    } {
      if (callbackBookkeepingPool.length) {
        const instance = callbackBookkeepingPool.pop();
        instance.topLevelType = topLevelType;
        instance.nativeEvent = nativeEvent;
        instance.targetInst = targetInst;
        return instance;
      }
      return {
        topLevelType,
        nativeEvent,
        targetInst,
        ancestors: [],
      };
    }
    • 这个函数就是一缓存机制
    • 最终我们在执行完整个react事件之后,会把这个对象再归还到这个pool里面来进行一个存储
    • 让浏览器的js运行环境不去删除这个对象,也让我们在后期不需要去重新创建一个对象,
    • 以减少这个对象声明和对象垃圾回收的一个性能开销
  • 进入 batchedUpdates

    js 复制代码
    // packages/events/ReactGenericBatching.js#L29
    let _batchedUpdatesImpl = function(fn, bookkeeping) {
      return fn(bookkeeping);
    };
    let isBatching = false;
    export function batchedUpdates(fn, bookkeeping) {
      if (isBatching) {
        // If we are currently inside another batch, we need to wait until it
        // fully completes before restoring state.
        return fn(bookkeeping);
      }
      isBatching = true;
      try {
        return _batchedUpdatesImpl(fn, bookkeeping);
      } finally {
        // Here we wait until all updates have propagated, which is important
        // when using controlled components within layers:
        // https://github.com/facebook/react/issues/1698
        // Then we restore state of any controlled component.
        isBatching = false;
        // 下面的代码其实就跟 input 控制输入 有关的
        // 我们知道在 react 当中我们通过 value 给 input 标签上面去绑定了值,直接在外部输入的值
        // 如果没有 onChange 事件来处理这个 state,它这个值输不进去的,这就是 input的控制输入 的一个概念
        // 其控制就是在这里实现的
        const controlledComponentsHavePendingUpdates = needsStateRestore();
        if (controlledComponentsHavePendingUpdates) {
          // If a controlled event was fired, we may need to restore the state of
          // the DOM node back to the controlled value. This is necessary when React
          // bails out of the update without touching the DOM.
          _flushInteractiveUpdatesImpl();
          restoreStateIfNeeded();
        }
      }
    }
    
    // packages/events/ReactControlledComponent.js#L58
    export function needsStateRestore(): boolean {
      return restoreTarget !== null || restoreQueue !== null;
    }
    
    export function restoreStateIfNeeded() {
      if (!restoreTarget) {
        return;
      }
      const target = restoreTarget;
      const queuedTargets = restoreQueue;
      restoreTarget = null;
      restoreQueue = null;
    
      restoreStateOfTarget(target);
      if (queuedTargets) {
        for (let i = 0; i < queuedTargets.length; i++) {
          restoreStateOfTarget(queuedTargets[i]);
        }
      }
    }
  • batchedUpdates 里面调用的方法是 handleTopLevel

    js 复制代码
    function handleTopLevel(bookKeeping) {
      let targetInst = bookKeeping.targetInst;
    
      // Loop through the hierarchy, in case there's any nested components.
      // It's important that we build the array of ancestors before calling any
      // event handlers, because event handlers can modify the DOM, leading to
      // inconsistencies with ReactMount's node cache. See #1105.
      let ancestor = targetInst;
      do {
        // 如果不存在,就push,并且跳出循环
        if (!ancestor) {
          bookKeeping.ancestors.push(ancestor);
          break;
        }
        // 如果存在了,找到 Root 挂载节点
        const root = findRootContainerNode(ancestor);
        // 不存在挂载节点,则跳出
        if (!root) {
          break;
        }
        // 存在挂载节点
        bookKeeping.ancestors.push(ancestor);
        // 这个方法之前已经分析了,找到存储的 fiber 对象
        // 在这里, 这个时候传入了是这个root,对于大部分情况来讲, HostRoot 已经没有上级的节点,会是处于react的一个应用当中
        // 也可能是会有这种情况的, 比如通过一些比较 hack 的一些方式, 我们在react应用里面再去渲染了一个新的react应用
        // 这种方法也可能是存在的,所以在这边就尝试了这么去做
        // 因为它们两个如果确实出现这种情况,那么它们的root节点是不一样的
        // 事件正常来讲是要冒泡到最外层的那个root树的最顶上的
        // 所以这种情况需要去调用这个方法,去把所有的 ancestor 给找到,并且推到这个 bookKeeping.ancestors 里面
        ancestor = getClosestInstanceFromNode(root); 
      } while (ancestor);
    
      // 对于每一个 concest,去获取它的 targetInst 对大部分情况, 其实就是我们这个 targetInst 
      // 就是我们这个事件触发的那一个节点 event.target 对应的那个fiber对象
      for (let i = 0; i < bookKeeping.ancestors.length; i++) {
        targetInst = bookKeeping.ancestors[i];
        runExtractedEventsInBatch(
          bookKeeping.topLevelType,
          targetInst,
          bookKeeping.nativeEvent,
          getEventTarget(bookKeeping.nativeEvent),
        );
      }
    }
    • 进入 findRootContainerNode

      js 复制代码
      function findRootContainerNode(inst) {
        // TODO: It may be a good idea to cache this to prevent unnecessary DOM
        // traversal, but caching is difficult to do correctly without using a
        // mutation observer to listen for all DOM changes.
        // 向上找
        while (inst.return) {
          inst = inst.return;
        }
        // 如果不是 HostRoot 则失败
        if (inst.tag !== HostRoot) {
          // This can happen if we're in a detached tree.
          return null;
        }
        // 找到 react 应用挂载的 dom节点
        return inst.stateNode.containerInfo;
      }
    • 进入 runExtractedEventsInBatch

      js 复制代码
      export function runExtractedEventsInBatch(
        topLevelType: TopLevelType,
        targetInst: null | Fiber,
        nativeEvent: AnyNativeEvent,
        nativeEventTarget: EventTarget,
      ) {
        // 获取所有事件
        const events = extractEvents(
          topLevelType,
          targetInst,
          nativeEvent,
          nativeEventTarget,
        );
        runEventsInBatch(events);
      }
      
      // 生成事件对象
      function extractEvents(
        topLevelType: TopLevelType,
        targetInst: null | Fiber,
        nativeEvent: AnyNativeEvent,
        nativeEventTarget: EventTarget,
      ): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
        let events = null;
        // 调用每一个plugin, 在内部,调用 possiblePlugin.extractEvents
        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) {
            const extractedEvents = possiblePlugin.extractEvents(
              topLevelType,
              targetInst,
              nativeEvent,
              nativeEventTarget,
            );
            // 如果存在,则插入到对象中
            if (extractedEvents) {
              events = accumulateInto(events, extractedEvents);
            }
          }
        }
        return events;
      }
      
      let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null;
      export function runEventsInBatch(
        events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null,
      ) {
        if (events !== null) {
          eventQueue = accumulateInto(eventQueue, events);
        }
      
        // Set `eventQueue` to null before processing it so that we can tell if more
        // events get enqueued while processing.
        const processingEventQueue = eventQueue;
        eventQueue = null;
      
        if (!processingEventQueue) {
          return;
        }
        // 对这个 processingEventQueue, 可能是数组,也可能只有一个event的这个内容
        // 对它调用了这个 executeDispatchesAndReleaseTopLevel 方法
        forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
        invariant(
          !eventQueue,
          'processEventQueue(): Additional events were enqueued while processing ' +
            'an event queue. Support for this has not yet been implemented.',
        );
        // This would be a good time to rethrow if any of the event handlers threw.
        rethrowCaughtError();
      }
      • 进入 accumulateInto

        js 复制代码
        // 把两个值(数组)合并,形成一个数组
        function accumulateInto<T>(
          current: ?(Array<T> | T),
          next: T | Array<T>,
        ): T | Array<T> {
          invariant(
            next != null,
            'accumulateInto(...): Accumulated items must not be null or undefined.',
          );
        
          if (current == null) {
            return next;
          }
        
          // Both are not empty. Warning: Never call x.concat(y) when you are not
          // certain that x is an Array (x could be a string with concat method).
          // 合并两个数组
          if (Array.isArray(current)) {
            if (Array.isArray(next)) {
              current.push.apply(current, next);
              return current;
            }
            current.push(next);
            return current;
          }
        
          if (Array.isArray(next)) {
            // A bit too dangerous to mutate `next`.
            return [current].concat(next);
          }
        
          return [current, next];
        }
      • 进入 forEachAccumulated

        js 复制代码
        // 对于传进来的一个数组,判断它是否是一个数组
        // 如果是数组就会去对每一项调用这个 callback
        // 如果它不是一个数组,就直接调用这个callback传入这个值就可以了
        function forEachAccumulated<T>(
          arr: ?(Array<T> | T),
          cb: (elem: T) => void,
          scope: ?any,
        ) {
          if (Array.isArray(arr)) {
            arr.forEach(cb, scope);
          } else if (arr) {
            cb.call(scope, arr);
          }
        }
      • 进入 executeDispatchesAndReleaseTopLevel

        js 复制代码
        const executeDispatchesAndReleaseTopLevel = function(e) {
          return executeDispatchesAndRelease(e);
        };
        
        const executeDispatchesAndRelease = function(event: ReactSyntheticEvent) {
          if (event) {
            executeDispatchesInOrder(event);
        
            if (!event.isPersistent()) {
              event.constructor.release(event);
            }
          }
        };
        
        // packages/events/EventPluginUtils.js#L76
        // 最后真正调用这个事件的地方,其实它里面整个过程会非常的复杂,有各种各样的函数的嵌套调用
        // 其实里面可能有将近一半的函数都是工具类型的函数,注意阅读代码时,别被绕进去
        export function executeDispatchesInOrder(event) {
          // 这边获取了 dispatchListeners 以及 dispatchInstances 这两个数据
          // 这两个数据都来自 event 对象,上面会挂载了一个叫 _dispatchListeners 和 _dispatchInstances
          // 这两个东西都是数组,并且是一一对应的关系
          const dispatchListeners = event._dispatchListeners;
          const dispatchInstances = event._dispatchInstances;
          // 忽略
          if (__DEV__) {
            validateEventDispatches(event);
          }
          // 然后,它去判断一下是否是一个数组, 如果是一个数组对它进行一个遍历
          if (Array.isArray(dispatchListeners)) {
            for (let i = 0; i < dispatchListeners.length; i++) {
              // 并且判断一下 event 是否已经 isPropagationStopped,就是我们已经停止冒泡了
              // 如果是的话,我们就直接 break
              if (event.isPropagationStopped()) {
                break;
              }
              // Listeners and Instances are two parallel arrays that are always in sync.
              executeDispatch(event, dispatchListeners[i], dispatchInstances[i]);
            }
          // 不是数组,但是存在
          } else if (dispatchListeners) {
            // 调用这个方法
            executeDispatch(event, dispatchListeners, dispatchInstances);
          }
          // 重置
          event._dispatchListeners = null;
          event._dispatchInstances = null;
        }
        • 进入 executeDispatch

          js 复制代码
          function executeDispatch(event, listener, inst) {
            const type = event.type || 'unknown-event';
            event.currentTarget = getNodeFromInstance(inst); // 从 inst 上获取 node 节点
            invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event); // 这里回调最终被触发调用
            event.currentTarget = null;
          }
          • 进入 invokeGuardedCallbackAndCatchFirstError

            js 复制代码
            // packages/shared/ReactErrorUtils.js#L67
            export function invokeGuardedCallbackAndCatchFirstError<
              A,
              B,
              C,
              D,
              E,
              F,
              Context,
            >(
              name: string | null,
              func: (a: A, b: B, c: C, d: D, e: E, f: F) => void,
              context: Context,
              a: A,
              b: B,
              c: C,
              d: D,
              e: E,
              f: F,
            ): void {
              // 这个函数之前遇到过,目前不再展开
              invokeGuardedCallback.apply(this, arguments);
              if (hasError) {
                const error = clearCaughtError();
                if (!hasRethrowError) {
                  hasRethrowError = true;
                  rethrowError = error;
                }
              }
            }
  • 到这里为止,这边的 listener 就已经被调用了

  • 我们真正的每一个节点上面,如果有绑定这个事件,它就会调用它的一个回调

  • 这就是事件触发的整个流程, 非常的繁琐,有各种各样的方法,嵌套的调用

相关推荐
neter.asia7 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫8 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
光影少年27 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_28 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891130 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾32 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking32 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu34 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym39 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化
~甲壳虫40 分钟前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js