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 就已经被调用了

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

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

相关推荐
y先森4 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy4 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189114 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿5 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡6 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇7 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒7 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员7 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐7 小时前
前端图像处理(一)
前端