想必大家都用过弹窗组件,比如 antd 的 Modal 组件:
打开 devtools 可以看到,它是直接挂在 body 下的:
实现这种效果是用的 createPortal:
渲染结果如下:
弹窗组件都是基于这个 api 来实现的。
那 React 源码里是如何实现这种功能的呢?
首先,我们过一遍 React 的渲染流程:
我们组件里写的这些是 jsx 代码:
它们编译后会变成类似 React.createElement 这种代码,叫做 render function。
render function 执行的结果是 React Element。
类似这样:
React 组件 render 的结果就是产生 React Element。
我们也经常把 React Element 叫做 vdom。
React 会把 vdom 转成 fiber 的结构,这个过程叫做 reconcile:
reconcile 完成后,就产生了完整的 fiber 树。
之后会经历 commit 阶段,遍历这个 fiber 树,进行 dom 的增删改,effect 执行等。
这就是 React 的渲染流程,也就是 render(reconcile) commit 2 大阶段。
接下来我们一起调试下 React 源码:
代码语言:javascript复制npx create-react-app --template typescript portal-test
先用 cra 创建个 react 项目。
进入项目,把 App.tsx 改成这样:
代码语言:javascript复制import { createPortal } from 'react-dom';
function App() {
return (
<div className="App">
<p>
bbbb
</p>
{createPortal(
<p>aaaa</p>,
document.body
)}
</div>
);
}
export default App;
把项目跑起来:
代码语言:javascript复制npm run start
然后新建一个调试配置:
点击 debug 面板的 create a lunch.json 按钮,创建一个 chrome 类型的调试配置。
把 url 改为 3000 端口:
在组件里打个断点:
点击调试启动:
代码在断点处断住了,调用栈的前面这些就是 react 源码:
从哪里看起呢?
肯定是从 createPortal 开始看啊。
搜索 createPortal,在这里打个断点:
其实看它的返回值就知道,这是一个 React Element,也就是 vdom,类型是 REACT_PORTAL_TYPE。
点击释放断点,代码就会执行到这里:
注意这个 containerInfo,它就是 createPortal 传入的第二个参数:
然后我们再看 vdom 转 fiber 的部分,也就是 REACT_PORTAL_TYPE 的 React Element 是怎么转成 fiber 节点的呢?
搜索这个 REACT_PORTAL_TYPE,你会找到它转成 fiber 的代码:
在这里打个断点。
释放断点,代码执行到这里,可以看到第一个参数就是要转换的 React Element 节点。
进入这个函数:
可以看到,它创建了一个 fiber 节点,并且把之前的 containerInfo 放到了 fiber.stateNode 上。
为什么是放在 fiber.stateNode 上呢?
想想我们有很多种类型的 fiber 节点,这些不同类型的 fiber 节点都有一些自己的数据要存,也就是状态数据。
那是不是就可以加一个 stateNode 属性来放各种类型的 fiber 节点的私有信息呢。
这就是 fiber.stateNode,各种类型的节点都在这里放东西,而且存的东西不同。
继续看这个 fiber 节点:
第一个参数是 tag,它是用来区分 fiber 节点类型的。
之前是用 React Element 的 $$typeof 属性区分,而之后就是用 fiber 的 tag 属性区分了:
就像前面所说,从 vdom(React Elment) 转 fiber 的过程,就是 reconcile 了。
看下调用栈也可以看出来:
之后就是 commit 阶段,看下 dom 节点是如何挂载的。
搜索 stateNode.containerInfo 可以找到处理它的代码:
可以看出来,这里是根据父 fiber 节点的类型,来判断如何插入这个节点。
在这里打个断点。
下面有个 insertOrAppendXxx 的方法,就是插入或者追加节点到 dom 的。
它会先查找当前节点的 before 的节点,如果存在就是 insert。
因为我们这里是 body,所以是 append:
执行完这一步之后,dom 就被插入到了 body 下:
这就是 creatPortal 的原理。
看下调用栈,插入 dom 这部分,就是 commit 阶段做的:
总结
弹窗组件会把 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 的实现原理。