TOP

React 扩展机制

React 提供了多种扩展机制,用于处理特殊场景:Portal 用于跨 DOM 树渲染,Profiler 用于性能测量,Error Boundary 用于错误捕获。

这些机制在源码中都有特定的生命周期和实现方式,理解它们有助于更好地使用 React 和排查问题。

Portal:跨 DOM 位置渲染

Portal 允许将子节点渲染到 DOM 树的不同位置,常用于模态框、工具提示等场景。

基本用法

import { createPortal } from 'react-dom';

function Modal({ children, isOpen }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal">{children}</div>,
    document.body, // 渲染到 body,而不是父组件的位置
  );
}

function App() {
  return (
    <div>
      <Modal isOpen={true}>
        <h1>Modal Content</h1>
      </Modal>
    </div>
  );
}

DOM 结构

<!-- React 树结构 -->
<div id="app">
  <div>
    <!-- Modal 组件在这里 -->
  </div>
</div>

<!-- 实际 DOM 结构 -->
<div id="app">
  <div></div>
</div>
<body>
  <div class="modal">
    <h1>Modal Content</h1>
  </div>
</body>

实现原理

function createPortal(children: ReactNode, container: DOMContainer, key?: string | null): ReactPortal {
  return {
    $$typeof: REACT_PORTAL_TYPE,
    key: key == null ? null : String(key),
    children,
    containerInfo: container,
    implementation: null,
  };
}

渲染过程

function commitPlacement(finishedWork: Fiber): void {
  const parentFiber = getHostParentFiber(finishedWork);

  let parent;
  let isContainer;

  const parentStateNode = parentFiber.stateNode;

  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      // Portal 的父节点是另一个 Portal 的容器
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    default:
      throw new Error('Invalid host parent fiber.');
  }

  if (finishedWork.flags & Placement) {
    const before = getHostSibling(finishedWork);
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

Portal 的特殊处理

function getHostParentFiber(fiber: Fiber): Fiber {
  let parent = fiber.return;

  while (parent !== null) {
    if (isHostParent(parent)) {
      return parent;
    }
    parent = parent.return;
  }

  throw new Error('Expected to find a host parent.');
}

function isHostParent(fiber: Fiber): boolean {
  return (
    fiber.tag === HostComponent || fiber.tag === HostRoot || fiber.tag === HostPortal // Portal 也是有效的父节点
  );
}

事件冒泡

Portal 中的事件会冒泡到 React 树,而不是 DOM 树。这是 React 事件系统的核心特性。

示例场景

function App() {
  function handleClick() {
    console.log('App clicked'); // 会触发
  }

  return (
    <div onClick={handleClick}>
      <Modal>
        <button>Click me</button>
      </Modal>
    </div>
  );
}

DOM 结构 vs React 树结构

DOM 结构(实际):
<body>
  <div id="app">
    <div>  <!-- App 的 div -->
    </div>
  </div>
  <div class="modal">  <!-- Portal 渲染到这里 -->
    <button>Click me</button>
  </div>
</body>

React 树结构(逻辑):
<div onClick={handleClick}>  <!-- App 的 div -->
  <Modal>
    <button>Click me</button>  <!-- Portal 中的按钮 -->
  </Modal>
</div>

事件冒泡路径

当点击 Portal 中的按钮时:

事件绑定过程

React 的事件绑定发生在组件挂载时,整个过程如下:

1. 首次渲染阶段

// JSX
<div onClick={handleClick}>
  <button>Click</button>
</div>

2. Commit Phase - Mutation 阶段

// React 内部处理
function commitMutationEffects(root, finishedWork) {
  switch (finishedWork.tag) {
    case HostComponent:
      // 处理 DOM 属性
      commitUpdate(instance, updatePayload, type, oldProps, newProps);
      // updatePayload 包含需要更新的属性,包括事件监听器
      break;
  }
}

// updatePayload 示例
// {
//   onClick: handleClick,  // 事件处理器函数
//   className: 'container',
//   ...
// }

3. 事件监听器的绑定

React 不会直接在 DOM 元素上绑定事件监听器,而是:

// React 17+ 的事件绑定(委托到根容器)
function listenToAllSupportedEvents(rootContainerElement) {
  // 1. 在根容器上绑定所有支持的事件类型
  allNativeEvents.forEach((domEventName) => {
    if (!nonDelegatedEvents.has(domEventName)) {
      // 2. 使用捕获阶段监听(保证先于原生事件处理)
      listenToNativeEvent(
        domEventName,
        false, // 不使用捕获
        rootContainerElement,
      );
    }
  });
}

// 监听函数
function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
  const listener = dispatchEvent.bind(null, domEventName);

  // 绑定到根容器
  if (isCapturePhaseListener) {
    target.addEventListener(domEventName, listener, true); // 捕获阶段
  } else {
    target.addEventListener(domEventName, listener, false); // 冒泡阶段
  }
}

4. 事件处理器的存储

事件处理器存储在 Fiber 节点的 memoizedProps 中:

// Fiber 节点结构
{
  tag: HostComponent,
  stateNode: divElement,  // DOM 元素
  memoizedProps: {
    onClick: handleClick,  // 事件处理器存储在这里
    className: 'container',
    children: ...
  },
  // ...
}

5. 完整绑定流程

组件渲染

Render Phase: 创建 Fiber 节点,props 包含 onClick

Commit Phase - Mutation:
  ├─ 创建/更新 DOM 元素
  ├─ props 应用到 DOM(但不包括事件处理器)
  └─ 事件处理器存储在 Fiber.memoizedProps

应用初始化时(只执行一次):
  ├─ listenToAllSupportedEvents(rootContainer)
  └─ 在根容器上绑定所有事件类型的监听器

事件触发时:
  ├─ 原生事件冒泡到根容器
  ├─ React 的事件监听器被触发
  ├─ dispatchEvent 查找目标 Fiber
  └─ 沿着 Fiber 树向上查找事件处理器

关键点

  1. 统一绑定:所有事件都绑定到根容器,而不是各个 DOM 元素
  2. 按需查找:事件触发时,通过 Fiber 树查找对应的事件处理器
  3. 性能优化:减少事件监听器的数量,提升性能
  4. 跨容器支持:Portal 中的事件也能被正确处理,因为都委托到根容器

实现原理:事件委托机制

React 使用事件委托(Event Delegation)机制:

  1. 统一委托:所有事件都委托到根容器(通常是 documentroot
  2. 捕获阶段:事件在捕获阶段被 React 捕获
  3. 查找 Fiber:通过 event.target 找到对应的 Fiber 节点
  4. 模拟冒泡:沿着 Fiber 树向上遍历,模拟 React 树的冒泡过程

通过原生事件触发的 DOM 找到对应的 Fiber 节点

function findInstanceBlockingEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
  return_targetInst = null; //  这是一个全局变量,用于存储找到的 Fiber 节点

  // 获取原生事件的目标 DOM 元素
  const nativeEventTarget = getEventTarget(nativeEvent);

  // 从 DOM 节点找到最近的 React Fiber 实例
  let targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null) {
    //  复杂处理,忽略
  }
  // 记录最终得到的目标 Fiber,用于后续事件分发
  return_targetInst = targetInst;
  return null;
}

找到目标 Fiber 节点后,开始事件分发

// React 事件系统的简化实现
function dispatchEvent(nativeEvent, targetFiber) {
  // 1. 从目标 Fiber 开始
  let fiber = targetFiber;

  // 2. 沿着 Fiber 树向上遍历(模拟冒泡)
  while (fiber !== null) {
    // 3. 检查是否有事件处理器
    const props = fiber.memoizedProps;
    if (props && props.onClick) {
      // 4. 执行事件处理器
      props.onClick(nativeEvent);
    }

    // 5. 继续向上遍历(包括 Portal)
    fiber = fiber.return; // 返回父节点(可能是 Portal)
  }
}

关键点

  1. Fiber 树遍历:事件冒泡沿着 Fiber 树(React 树)进行,而不是 DOM 树
  2. Portal 透明:Portal 在事件冒泡中是透明的,不会中断冒泡路径
  3. 统一处理:所有事件都通过同一个委托机制处理,保证一致性

为什么这样设计?

  1. 一致性:Portal 中的组件行为与普通组件一致
  2. 灵活性:可以在 Portal 外部处理 Portal 内部的事件
  3. 性能:事件委托减少事件监听器的数量,提升性能

使用场景

场景推荐原因
模态框 Portal避免 z-index 问题
工具提示 Portal避免 overflow 裁剪
下拉菜单 Portal避免父容器限制
普通组件 Portal增加复杂度

Profiler:性能测量

Profiler 用于测量 React 组件的渲染性能,帮助识别性能瓶颈。

基本用法

import { Profiler } from 'react';

function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  console.log('Profiler:', {
    id,
    phase, // 'mount' 或 'update'
    actualDuration, // 实际渲染时间
    baseDuration, // 估计渲染时间(无 memo)
    startTime, // 开始时间
    commitTime, // 提交时间
  });
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <ExpensiveComponent />
    </Profiler>
  );
}

实现原理

function updateProfiler(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes): Fiber | null {
  const nextProps = workInProgress.pendingProps;
  const onRender = nextProps.onRender;

  if (onRender !== null && typeof onRender === 'function') {
    // 记录开始时间
    workInProgress.memoizedProps = {
      ...nextProps,
      onRender,
    };
  } else {
    // 开发环境警告
    if (__DEV__) {
      console.warn('Profiler onRender prop is not a function.');
    }
  }

  // 处理子节点
  reconcileChildren(current, workInProgress, nextProps.children, renderLanes);
  return workInProgress.child;
}

性能测量

function commitProfilerEffect(finishedWork: Fiber): void {
  const { onRender, id } = finishedWork.memoizedProps;

  if (typeof onRender === 'function') {
    const { effectDuration } = finishedWork;
    const { actualDuration, baseDuration, treeBaseDuration } = effectDuration;

    // 调用回调
    onRender(
      id,
      finishedWork.mode & ProfileMode ? 'mount' : 'update',
      actualDuration,
      baseDuration,
      finishedWork.actualStartTime,
      getCommitTime(),
    );
  }
}

性能数据解读

function onRenderCallback(
  id,
  phase,
  actualDuration, // 实际渲染时间(包括子组件)
  baseDuration, // 估计渲染时间(不包括 memo 优化)
  startTime,
  commitTime,
) {
  // actualDuration > baseDuration:可能有性能问题
  // actualDuration < baseDuration:使用了 memo 优化
}

优化建议

情况可能原因优化方案
actualDuration 很大组件渲染慢使用 React.memouseMemo
baseDuration 很大组件本身复杂拆分组件、优化算法
actualDuration >> baseDuration子组件重复渲染使用 React.memouseMemo

使用场景

// 场景 1:测量特定组件
<Profiler id="ExpensiveComponent" onRender={onRenderCallback}>
  <ExpensiveComponent />
</Profiler>

// 场景 2:测量整个应用
<Profiler id="App" onRender={onRenderCallback}>
  <App />
</Profiler>

// 场景 3:嵌套测量
<Profiler id="App" onRender={onRenderCallback}>
  <Profiler id="Header" onRender={onRenderCallback}>
    <Header />
  </Profiler>
  <Profiler id="Content" onRender={onRenderCallback}>
    <Content />
  </Profiler>
</Profiler>

Error Boundary:错误边界

Error Boundary 用于捕获子组件树中的错误,防止整个应用崩溃。

基本用法

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // 更新 state,显示错误 UI
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 记录错误信息
    console.error('Error caught:', error, errorInfo);
    logErrorToService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }

    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

实现原理

错误捕获

function commitRoot(root: FiberRoot, finishedWork: Fiber) {
  // 在 Commit Phase 捕获错误
  commitBeforeMutationEffects(root, finishedWork);
  commitMutationEffects(root, finishedWork, committedLanes);
  commitLayoutEffects(finishedWork, root, committedLanes);

  // 检查是否有错误
  if (root.capturedErrors !== null) {
    const errors = root.capturedErrors;
    root.capturedErrors = null;

    // 处理错误
    commitErrorHandling(root, errors);
  }
}

错误处理

function commitErrorHandling(root: FiberRoot, errors: Array<CapturedValue>) {
  for (let i = 0; i < errors.length; i++) {
    const error = errors[i];
    const errorBoundary = findErrorBoundary(root.current, error);

    if (errorBoundary !== null) {
      // 找到错误边界,调用生命周期
      const errorInfo = {
        componentStack: getComponentStack(errorBoundary),
      };

      if (errorBoundary.tag === ClassComponent) {
        const instance = errorBoundary.stateNode;
        instance.componentDidCatch(error, errorInfo);

        // 更新状态
        const newState = getDerivedStateFromError(errorBoundary, error);
        if (newState !== null) {
          updateClassComponent(errorBoundary, errorBoundary.elementType, errorBoundary.memoizedProps, newState);
        }
      }
    } else {
      // 没有错误边界,应用崩溃
      throw error;
    }
  }
}

查找错误边界

function findErrorBoundary(fiber: Fiber, error: any): Fiber | null {
  let node = fiber;

  while (node !== null) {
    if (node.tag === ClassComponent) {
      const ctor = node.type;
      const instance = node.stateNode;

      // 检查是否有 componentDidCatch 或 getDerivedStateFromError
      if (
        typeof ctor.getDerivedStateFromError === 'function' ||
        (instance !== null &&
          typeof instance.componentDidCatch === 'function' &&
          !isAlreadyFailedLegacyErrorBoundary(instance))
      ) {
        return node; // 找到错误边界
      }
    }

    node = node.return;
  }

  return null; // 没有找到错误边界
}

错误边界限制

Error Boundary 不能捕获以下错误:

错误类型是否捕获原因
渲染错误主要用途
事件处理器错误在事件处理中,不在渲染中
异步代码错误setTimeout、Promise 等
服务端渲染错误SSR 不支持
错误边界自身错误会向上传播
function BuggyComponent() {
  //  会被捕获
  if (Math.random() > 0.5) {
    throw new Error('Render error');
  }

  //  不会被捕获
  function handleClick() {
    throw new Error('Event error');
  }

  //  不会被捕获
  useEffect(() => {
    throw new Error('Effect error');
  }, []);

  return <button onClick={handleClick}>Click</button>;
}

函数组件错误边界

函数组件不能直接作为错误边界,需要使用类组件或第三方库:

//  函数组件不能作为错误边界
function ErrorBoundary({ children }) {
  // 无法使用 getDerivedStateFromError 和 componentDidCatch
  return children;
}

//  使用类组件
class ErrorBoundary extends React.Component {
  // ...
}

//  使用第三方库(如 react-error-boundary)
import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorFallback />}>
      <BuggyComponent />
    </ErrorBoundary>
  );
}

使用场景

// 场景 1:全局错误边界
function App() {
  return (
    <ErrorBoundary fallback={<AppErrorFallback />}>
      <Router>
        <Routes />
      </Router>
    </ErrorBoundary>
  );
}

// 场景 2:局部错误边界
function Dashboard() {
  return (
    <div>
      <ErrorBoundary fallback={<WidgetErrorFallback />}>
        <Widget1 />
      </ErrorBoundary>
      <ErrorBoundary fallback={<WidgetErrorFallback />}>
        <Widget2 />
      </ErrorBoundary>
    </div>
  );
}

// 场景 3:嵌套错误边界
function App() {
  return (
    <ErrorBoundary fallback={<AppErrorFallback />}>
      <Header />
      <ErrorBoundary fallback={<ContentErrorFallback />}>
        <Content />
      </ErrorBoundary>
      <Footer />
    </ErrorBoundary>
  );
}

总结

React 的扩展机制提供了强大的能力:

  1. Portal:跨 DOM 位置渲染,解决 z-index、overflow 等问题
  2. Profiler:性能测量,帮助识别和优化性能瓶颈
  3. Error Boundary:错误捕获,提高应用的健壮性

这些机制在源码中都有特定的实现方式,理解它们有助于:

通过合理使用这些机制,可以构建更强大、更稳定的 React 应用。