React 弹窗组件用的 createPortal 是怎么实现的?
  hINapgLEIiPz 2023年11月19日 37 0

想必大家都用过弹窗组件,比如 antd 的 Modal 组件:

React 弹窗组件用的 createPortal 是怎么实现的?_前端

打开 devtools 可以看到,它是直接挂在 body 下的:

React 弹窗组件用的 createPortal 是怎么实现的?_前端_02

实现这种效果是用的 createPortal:

React 弹窗组件用的 createPortal 是怎么实现的?_前端_03

渲染结果如下:

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_04

弹窗组件都是基于这个 api 来实现的。

那 React 源码里是如何实现这种功能的呢?

首先,我们过一遍 React 的渲染流程:

我们组件里写的这些是 jsx 代码:

React 弹窗组件用的 createPortal 是怎么实现的?_App_05

它们编译后会变成类似 React.createElement 这种代码,叫做 render function。

render function 执行的结果是 React Element。

类似这样:

React 弹窗组件用的 createPortal 是怎么实现的?_App_06

React 组件 render 的结果就是产生 React Element。

我们也经常把 React Element 叫做 vdom。

React 会把 vdom 转成 fiber 的结构,这个过程叫做 reconcile:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_07

reconcile 完成后,就产生了完整的 fiber 树。

之后会经历 commit 阶段,遍历这个 fiber 树,进行 dom 的增删改,effect 执行等。

这就是 React 的渲染流程,也就是 render(reconcile) + commit 2 大阶段。

接下来我们一起调试下 React 源码:

npx create-react-app --template typescript portal-test

先用 cra 创建个 react 项目。

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_08

进入项目,把 App.tsx 改成这样:

import { createPortal } from 'react-dom';

function App() {
  return (
    <div className="App">
        <p>
          bbbb
        </p>
        {createPortal(
          <p>aaaa</p>,
          document.body
        )}
    </div>
  );
}

export default App;

把项目跑起来:

npm run start

React 弹窗组件用的 createPortal 是怎么实现的?_App_09

然后新建一个调试配置:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_10

点击 debug 面板的 create a lunch.json 按钮,创建一个 chrome 类型的调试配置。

把 url 改为 3000 端口:

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_11

在组件里打个断点:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_12

点击调试启动:

React 弹窗组件用的 createPortal 是怎么实现的?_JavaScript_13

代码在断点处断住了,调用栈的前面这些就是 react 源码:

React 弹窗组件用的 createPortal 是怎么实现的?_App_14

从哪里看起呢?

肯定是从 createPortal 开始看啊。

搜索 createPortal,在这里打个断点:

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_15

其实看它的返回值就知道,这是一个 React Element,也就是 vdom,类型是 REACT_PORTAL_TYPE。

点击释放断点,代码就会执行到这里:

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_16

注意这个 containerInfo,它就是 createPortal 传入的第二个参数:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_17

然后我们再看 vdom 转 fiber 的部分,也就是 REACT_PORTAL_TYPE 的 React Element 是怎么转成 fiber 节点的呢?

搜索这个 REACT_PORTAL_TYPE,你会找到它转成 fiber 的代码:

React 弹窗组件用的 createPortal 是怎么实现的?_前端_18

在这里打个断点。

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_19

释放断点,代码执行到这里,可以看到第一个参数就是要转换的 React Element 节点。

进入这个函数:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_20

可以看到,它创建了一个 fiber 节点,并且把之前的 containerInfo 放到了 fiber.stateNode 上。

为什么是放在 fiber.stateNode 上呢?

想想我们有很多种类型的 fiber 节点,这些不同类型的 fiber 节点都有一些自己的数据要存,也就是状态数据。

那是不是就可以加一个 stateNode 属性来放各种类型的 fiber 节点的私有信息呢。

这就是 fiber.stateNode,各种类型的节点都在这里放东西,而且存的东西不同。

继续看这个 fiber 节点:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_21

React 弹窗组件用的 createPortal 是怎么实现的?_JavaScript_22

第一个参数是 tag,它是用来区分 fiber 节点类型的。

之前是用 React Element 的 $$typeof 属性区分,而之后就是用 fiber 的 tag 属性区分了:

React 弹窗组件用的 createPortal 是怎么实现的?_搜索_23

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_24

就像前面所说,从 vdom(React Elment) 转 fiber 的过程,就是 reconcile 了。

看下调用栈也可以看出来:

React 弹窗组件用的 createPortal 是怎么实现的?_JavaScript_25

之后就是 commit 阶段,看下 dom 节点是如何挂载的。

搜索 stateNode.containerInfo 可以找到处理它的代码:

React 弹窗组件用的 createPortal 是怎么实现的?_React.js_26

可以看出来,这里是根据父 fiber 节点的类型,来判断如何插入这个节点。

在这里打个断点。

下面有个 insertOrAppendXxx 的方法,就是插入或者追加节点到 dom 的。

它会先查找当前节点的 before 的节点,如果存在就是 insert。

因为我们这里是 body,所以是 append:

React 弹窗组件用的 createPortal 是怎么实现的?_App_27

执行完这一步之后,dom 就被插入到了 body 下:

React 弹窗组件用的 createPortal 是怎么实现的?_前端_28

这就是 creatPortal 的原理。

看下调用栈,插入 dom 这部分,就是 commit 阶段做的:

React 弹窗组件用的 createPortal 是怎么实现的?_App_29

总结

弹窗组件会把 dom 渲染到 body 下,这需要用到 createPortal 的 api。

我们一起调试源码来探究了它的实现原理。

react 的 jsx 语法编译之后会变成 render function 的代码,执行后产生 React Element(也就是 vdom)。

createPortal 的返回值就是一种 React Element 节点,其中 containerInfo 存放着容器节点。

之后 react 会进行 reconcile,也就是 React Element 转 fiber 的过程。

portal 对应的 React Element 节点会转成对应的 fiber 节点,containerInfo 会放到 fiber.stateNode 属性上。

之后进入 commit 阶段,会遍历 fiber 树进行 dom 的增删改。

处理到 portal 的 fiber 节点,就会执行 append 的操作。

这样,createPortal 第一个参数的节点,就会挂到 body 下。

这就是弹窗组件依赖的 createPortal api 的实现原理。

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月19日 0

暂无评论

推荐阅读
  f18CFixvrKz8   2024年05月20日   88   0   0 JavaScript
  fxrR9b8fJ5Wh   2024年05月17日   52   0   0 JavaScript
  2xk0JyO908yA   2024年04月28日   40   0   0 JavaScript
hINapgLEIiPz