React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React 不冒泡的移入 / 移出事件

React 不冒泡的移入 / 移出事件插件的实现

作者:YFF菲菲兔

本文主要介绍了React 不冒泡的移入 / 移出事件插件的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

基于原生 over/out 事件计算鼠标 / 指针进出关系,屏蔽原生冒泡缺陷、抹平浏览器与 SSR / 并发渲染差异,生成语义化非冒泡 onMouseEnter/onMouseLeave/onPointerEnter/onPointerLeave 合成事件,配套专用 Fiber 遍历、回调收集逻辑,支撑页面 hover 悬浮、移入移出交互,大幅降低拖拽、绘图、hover 悬浮等高频率场景计算开销

注册事件:registerEvents

浏览器原生 mouseenter/mouseleave、pointerenter/pointerleave 不冒泡,无法用 React 根容器事件委托实现,且原生事件不会跨 Portal、跨根容器正确隔离。React 不直接监听 enter/leave,转而监听会冒泡的 mouseover/mouseout / pointerover/pointerout,在插件内部手动模拟 enter/leave 语义

function registerEvents() {
  // 模拟 onMouseEnter / onMouseLeave,依赖原生 mouseover、mouseout
  registerDirectEvent('onMouseEnter', ['mouseout', 'mouseover']);
  registerDirectEvent('onMouseLeave', ['mouseout', 'mouseover']);
  // 模拟 onPointerEnter / onPointerLeave,依赖原生 pointerover、pointerout
  registerDirectEvent('onPointerEnter', ['pointerout', 'pointerover']);
  registerDirectEvent('onPointerLeave', ['pointerout', 'pointerover']);
}

提取事件:extractEvents

function extractEvents(
  dispatchQueue,
  // 原生事件名 mouseover/mouseout/pointerover/pointerout
  domEventName,
  // 触发事件DOM对应的Fiber
  targetInst,
  // 原生DOM Event对象
  nativeEvent,
  // 原生触发DOM
  nativeEventTarget,
  eventSystemFlags,
  // React根容器DOM
  targetContainer,
) {
  const isOverEvent =
    domEventName === 'mouseover' || domEventName === 'pointerover';
  const isOutEvent =
    domEventName === 'mouseout' || domEventName === 'pointerout';

  if (isOverEvent && !isReplayingEvent(nativeEvent)) {
    const related = nativeEvent.relatedTarget || nativeEvent.fromElement;
    if (related) {
      if (
        getClosestInstanceFromNode(related) ||
        isContainerMarkedAsRoot(related)
      ) {
        return;
      }
    }
  }

  if (!isOutEvent && !isOverEvent) {
    return;
  }

  let win;
  if (nativeEventTarget.window === nativeEventTarget) {
    win = nativeEventTarget;
  } else {
    const doc = nativeEventTarget.ownerDocument;
    if (doc) {
      win = doc.defaultView || doc.parentWindow;
    } else {
      win = window;
    }
  }

  let from;
  let to;
  if (isOutEvent) {
    const related = nativeEvent.relatedTarget || nativeEvent.toElement;
    from = targetInst;
    to = related ? getClosestInstanceFromNode(related) : null;
    if (to !== null) {
      const nearestMounted = getNearestMountedFiber(to);
      const tag = to.tag;
      if (
        to !== nearestMounted ||
        (tag !== HostComponent && tag !== HostSingleton && tag !== HostText)
      ) {
        to = null;
      }
    }
  } else {
    from = null;
    to = targetInst;
  }

  if (from === to) {
    return;
  }

  let SyntheticEventCtor = SyntheticMouseEvent;
  let leaveEventType = 'onMouseLeave';
  let enterEventType = 'onMouseEnter';
  let eventTypePrefix = 'mouse';
  if (domEventName === 'pointerout' || domEventName === 'pointerover') {
    SyntheticEventCtor = SyntheticPointerEvent;
    leaveEventType = 'onPointerLeave';
    enterEventType = 'onPointerEnter';
    eventTypePrefix = 'pointer';
  }

  const fromNode = from == null ? win : getNodeFromInstance(from);
  const toNode = to == null ? win : getNodeFromInstance(to);

  const leave = new SyntheticEventCtor(
    leaveEventType,
    eventTypePrefix + 'leave',
    from,
    nativeEvent,
    nativeEventTarget,
  );
  leave.target = fromNode;
  leave.relatedTarget = toNode;

  let enter = null;

  const nativeTargetInst = getClosestInstanceFromNode(nativeEventTarget);
  if (nativeTargetInst === targetInst) {
    const enterEvent = new SyntheticEventCtor(
      enterEventType,
      eventTypePrefix + 'enter',
      to,
      nativeEvent,
      nativeEventTarget,
    );
    enterEvent.target = toNode;
    enterEvent.relatedTarget = fromNode;
    enter = enterEvent;
  }

  accumulateEnterLeaveTwoPhaseListeners(dispatchQueue, leave, enter, from, to);
}

作用

设计意义

解决原生 mouseover 冒泡冗余触发问题:原生 mouseover 会层层冒泡,父、子元素都会触发 over 事件,当来源是 React 内部 DOM,直接放弃生成 enter 事件

辅助函数

移入移出两阶段事件回调收集:accumulateEnterLeaveTwoPhaseListeners

专门处理 mouseover/mouseout / pointerover/pointerout 这类带 relatedTarget 的移入移出事件

export function accumulateEnterLeaveTwoPhaseListeners(
  dispatchQueue,
  // 移出侧合成事件模板(mouseout / pointerout)
  leaveEvent,
  // 移入侧合成事件模板(mouseover / pointerover)
  enterEvent,
  // relatedTarget,鼠标离开的 DOM 对应 Fiber
  from,
  // 当前触发事件的 DOM 对应 Fiber
  to,
) {
  // from 和 to 的最低公共祖先 Fiber,作为遍历终止边界
  const common =
    from && to ? getLowestCommonAncestor(from, to, getParent) : null;

  if (from !== null) {
    // 单方向收集移入 / 移出回调
    accumulateEnterLeaveListenersForEvent(
      dispatchQueue,
      leaveEvent,
      from,
      common,
      false,
    );
  }
  if (to !== null && enterEvent !== null) {
    accumulateEnterLeaveListenersForEvent(
      dispatchQueue,
      enterEvent,
      to,
      common,
      true,
    );
  }
}

设计意义

鼠标从 A 元素移动到 B 元素时,两者公共祖先以上的组件不会同时触发移入 + 移出,只需遍历到公共祖先就停止,划定移入、移出遍历的终止边界,防止公共祖先上层组件重复触发事件,逻辑错乱,同时大幅减少 Fiber 遍历次数,优化性能,提升 mousemove 高频场景性能,大幅降低连续 mousemove 拖拽、绘图场景的计算开销

移入/移出单条链路的回调收集:accumulateEnterLeaveListenersForEvent

专门处理 mouseover/mouseout / pointerover/pointerout 移入 / 移出单条链路的回调收集

function accumulateEnterLeaveListenersForEvent(
  dispatchQueue,
  event,
  target,
  common,
  inCapturePhase,
) {
  const registrationName = event._reactName;
  const listeners = [];

  let instance = target;
  while (instance !== null) {
    if (instance === common) {
      break;
    }
    const {alternate, stateNode, tag} = instance;
    if (alternate !== null && alternate === common) {
      break;
    }
    if (
      (tag === HostComponent ||
        tag === HostHoistable ||
        tag === HostSingleton) &&
      stateNode !== null
    ) {
      const currentTarget = stateNode;
      if (inCapturePhase) {
        const captureListener = getListener(instance, registrationName);
        if (captureListener != null) {
          listeners.unshift(
            createDispatchListener(instance, captureListener, currentTarget),
          );
        }
      } else if (!inCapturePhase) {
        const bubbleListener = getListener(instance, registrationName);
        if (bubbleListener != null) {
          listeners.push(
            createDispatchListener(instance, bubbleListener, currentTarget),
          );
        }
      }
    }
    instance = instance.return;
  }
  if (listeners.length !== 0) {
    dispatchQueue.push({event, listeners});
  }
}

作用

查找 DOM 对应的 Fiber:getClosestInstanceFromNode

输入一个原生 DOM 节点,向上逐层查找距离该节点最近、由 React 管理的 Fiber 实例

export function getClosestInstanceFromNode(targetNode) {
  let targetInst;
  if (enableInternalInstanceMap) {
    // 使用 WeakMap(internalInstanceMap) 存储 DOM → Fiber(现代模式,不污染 DOM 属性)
    targetInst = internalInstanceMap.get(targetNode);
  } else {
    // 直接在 DOM 对象挂载内部 Symbol 键(旧模式)
    targetInst = targetNode[internalInstanceKey];
  }
  if (targetInst) {
    return targetInst;
  }
  let parentNode = targetNode.parentNode;
  while (parentNode) {

    if (enableInternalInstanceMap) {
      // internalContainerInstanceKey:React 根容器 / Portal 容器标记
      targetInst =
        parentNode[internalContainerInstanceKey] ||
        internalInstanceMap.get(parentNode);
    } else {
      targetInst =
        parentNode[internalContainerInstanceKey] ||
        parentNode[internalInstanceKey];
    }
    if (targetInst) {

      const alternate = targetInst.alternate;
      if (
        targetInst.child !== null ||
        (alternate !== null && alternate.child !== null)
      ) {

        let hydrationInstance = getParentHydrationBoundary(targetNode);
        while (hydrationInstance !== null) {
 
          const targetFiber = enableInternalInstanceMap
            ? internalInstanceMap.get(hydrationInstance)
            : hydrationInstance[internalInstanceKey];
          if (targetFiber) {
            return targetFiber;
          }
          hydrationInstance = getParentHydrationBoundary(hydrationInstance);
        }
      }
      return targetInst;
    }
    targetNode = parentNode;
    parentNode = targetNode.parentNode;
  }
  return null;
}

定位 SSR 水合区块顶层边界:getParentHydrationBoundary

SSR 水合注释标记机制:React SSR 服务端输出 HTML 时,会插入特殊 HTML 注释节点作为分割标记(水合边界标记),用来识别 Suspense/Activity 边界

向左遍历同层前置兄弟注释,匹配 Suspense/Activity 边界专属注释标记,找到当前节点所属的水合区块起始注释节点;用于修复水合阶段 DOM ↔ Fiber 映射错位问题。天然区分 CSR / SSR 环境,CSR 页面无任何 SSR 注释,遍历后直接返回 null,仅 SSR 页面、水合阶段才会返回有效边界注释

export function getParentHydrationBoundary(
  targetInstance,
) {
  // 从当前节点的前一个兄弟节点开始向左遍历
  let node = targetInstance.previousSibling;
  while (node) {
    if (node.nodeType === COMMENT_NODE) {
      const data = node.data;
      // 匹配各类 Suspense / Activity 起始注释
      if (
        data === SUSPENSE_START_DATA ||
        data === SUSPENSE_FALLBACK_START_DATA ||
        data === SUSPENSE_PENDING_START_DATA ||
        data === SUSPENSE_QUEUED_START_DATA ||
        data === ACTIVITY_START_DATA
      ) {
        // 找到顶层边界(depth归零),直接返回该注释节点作为水合边界
        if (depth === 0) {
          return node;
        } else {
          // 内层嵌套边界,层级-1,继续往左找外层边界
          depth--;
        }
      } else if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
        // 遇到结束注释,说明进入嵌套区块,层级+1
        depth++;
      }
    }
    // 继续向左遍历上一个兄弟
    node = node.previousSibling;
  }
  // 同层没有找到任何水合起始注释,返回null
  return null;
}

查找完成挂载的最近祖先 Fiber:getNearestMountedFiber

输入任意 Fiber,向上查找最近一个已经完成挂载、稳定可用的祖先 Fiber;若最终根节点不是 HostRoot(脱离 React 树),返回 null

export function getNearestMountedFiber(fiber) {
  let node = fiber;
  let nearestMounted;
  let nextNode = node;
  while (nextNode && !nextNode.alternate) {
    node = nextNode;
    // 判断当前节点是否处于新增/水合未完成状态
    if ((node.flags & (Placement | Hydrating)) !== NoFlags) {
      // 当前节点未挂载,它的父节点是候选稳定节点
      nearestMounted = node.return;
    }
    // 向上走父节点
    nextNode = node.return;
  }
  while (node.return) {
    node = node.return;
  }
  if (node.tag === HostRoot) {
    return nearestMounted;
  }
  return null;
}

到此这篇关于React 不冒泡的移入 / 移出事件插件的实现的文章就介绍到这了,更多相关React 不冒泡的移入 / 移出事件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文