React16源码: React中的PortalComponent创建, 调和, 更新的源码实现

PortalComponent

1 )概述

  • React Portal之所以叫Portal,因为做的就是和"传送门"一样的事情
  • render到一个组件里面去,实际改变的是网页上另一处的DOM结构
  • 主要关注 portal的创建, 调和, 更新过程

2 )源码

定位到 packages/react-dom/src/client/ReactDOM.js#L576

js 复制代码
function createPortal(
  children: ReactNodeList,
  container: DOMContainer,
  key: ?string = null,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  // TODO: pass ReactDOM portal implementation as third argument
  return ReactPortal.createPortal(children, container, null, key);
}
  • 这里调用的是 ReactPortal.createPortal, 进入

    js 复制代码
    // packages/shared/ReactPortal.js#L14
    export function createPortal(
      children: ReactNodeList,
      containerInfo: any,
      // TODO: figure out the API for cross-renderer implementation.
      implementation: any,
      key: ?string = null,
    ): ReactPortal {
      return {
        // This tag allow us to uniquely identify this as a React Portal
        $$typeof: REACT_PORTAL_TYPE,
        key: key == null ? null : '' + key,
        children,
        containerInfo, // dom 挂载节点
        implementation,
      };
    }
    • 这里返回一个对象,类似于 ReactElement
    • 区别在于 $$typeofcontainerInfo 需要的挂载点
  • 对于 REACT_PORTAL_TYPE 类型的组件在 reconcile 时, 看下具体操作, 找到 reconcileSinglePortal

    js 复制代码
    // packages/react-reconciler/src/ReactChildFiber.js#L1171
    function reconcileSinglePortal(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      portal: ReactPortal,
      expirationTime: ExpirationTime,
    ): Fiber {
      const key = portal.key; // 当前的 key
      let child = currentFirstChild;
      // 如果 存在 child, 对比 child.key === key
      while (child !== null) {
        // TODO: If key === null and child.key === null, then this only applies to
        // the first item in the list.
        if (child.key === key) {
          // 关注这里,有 containerInfo 的对比,portal 需要关心渲染到的节点是否有变化
          // 如果节点有变化,那么这个 portal 的渲染过程也会有变化
          // 都符合,说明老节点都可以复用
          if (
            child.tag === HostPortal &&
            child.stateNode.containerInfo === portal.containerInfo &&
            child.stateNode.implementation === portal.implementation
          ) {
            deleteRemainingChildren(returnFiber, child.sibling); // 删除其他节点
            // 通过 useFiber 复用这个节点
            const existing = useFiber(
              child,
              portal.children || [],
              expirationTime,
            );
            existing.return = returnFiber;
            return existing;
          } else {
            // key相同,但是不符合上述if, 没法复用,删除干净
            deleteRemainingChildren(returnFiber, child);
            break;
          }
        } else {
          // key 不同,删除当前节点
          deleteChild(returnFiber, child);
        }
        child = child.sibling;
      }
    
      // 创建一个 portal 的 fiber 对象
      const created = createFiberFromPortal(
        portal,
        returnFiber.mode,
        expirationTime,
      );
      created.return = returnFiber;
      return created;
    }
  • 对于更新一个portal节点,进入 updatePortalComponent

    js 复制代码
    // packages/react-reconciler/src/ReactFiberBeginWork.js#L1322
    function updatePortalComponent(
      current: Fiber | null,
      workInProgress: Fiber,
      renderExpirationTime: ExpirationTime,
    ) {
      // 挂载点处理
      pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo);
      const nextChildren = workInProgress.pendingProps; // 这里 pendingProps 是 children
      if (current === null) {
        // Portals are special because we don't append the children during mount
        // but at commit. Therefore we need to track insertions which the normal
        // flow doesn't do during mount. This doesn't happen at the root because
        // the root always starts with a "current" with a null child.
        // TODO: Consider unifying this with how the root works.
        workInProgress.child = reconcileChildFibers(
          workInProgress,
          null,
          nextChildren,
          renderExpirationTime,
        );
      } else {
        reconcileChildren(
          current,
          workInProgress,
          nextChildren,
          renderExpirationTime,
        );
      }
      return workInProgress.child;
    }
    • 之前梳理过的 API 不再赘述
    • 一些API的调用,在源码的注释中
相关推荐
左耳咚2 分钟前
项目开发中从补码到精度丢失的陷阱
前端·javascript·面试
黑云压城After14 分钟前
vue2实现图片自定义裁剪功能(uniapp)
java·前端·javascript
芙蓉王真的好118 分钟前
NestJS API 提示信息规范:让日志与前端提示保持一致的方法
前端·状态模式
dwedwswd25 分钟前
技术速递|从 0 到 1:用 Playwright MCP 搭配 GitHub Copilot 搭建 Web 应用调试环境
前端·github·copilot
2501_9387742939 分钟前
Leaflet 弹出窗实现:Spring Boot 传递省级旅游口号信息的前端展示逻辑
前端·spring boot·旅游
meichaoWen1 小时前
【CSS】CSS 面试知多少
前端·css
我血条子呢1 小时前
【预览PDF】前端预览pdf
前端·pdf·状态模式
90后的晨仔1 小时前
报错 找不到“node”的类型定义文件。 程序包含该文件是因为: 在 compilerOptions 中指定的类型库 "node" 的入口点 。
前端
90后的晨仔2 小时前
5分钟搭建你的第一个TypeScript项目
前端·typescript
专注前端30年2 小时前
Vue2 中 v-if 与 v-show 深度对比及实战指南
开发语言·前端·vue