使用 React 时,一般来说,使用函数组件return
一个元素时,该元素是被挂载到其最近的 DOM 父节点下,例如:
return (
<div>
{this.props.children}
</div>
);
挂载了一个新的div
元素,其子元素被渲染在此div
中。
但是,我们需要将子元素挂载到其他的任意 DOM 节点下。
例如,最近在实习中遇到一个的问题,要写一个鼠标悬浮时向下弹出选择框的效果。实际效果却是选择框向下弹出,但只显示出了一部分,有一部分不可见。
打开控制台后发现是由于上层组件元素使用了overflow: hidden
,导致溢出部分被隐藏了。本来想写一个样式覆盖掉overflow: hidden
,后来发现上层组件几乎每一层都使用了overflow: hidden
。组件设计时就默认这么写的,不可能每一层都去改样式😂。
这时候,我们就需要将子元素挂载到其他的 DOM 节点下。绕过上层使用了overflow: hidden
的元素,直接挂载到更上层。后来通过 Portals 就解决了这个问题。
作用
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
以上是React官网对 Portal 的描述。简单来说,可以将子元素渲染到其他的 DOM 节点。
用法
ReactDOM.createPortal(child, container)
child
:ReactNode,可渲染的 React 子元素container
:Element,DOM 元素
示例
如下代码所示,App
组件是一个大盒子,App
组件下有一个子组件PortalTest
:
const App = () => {
return (
<div className={s.AppBox} id='app'>
<PortalTest />
</div>
);
};
子组件PortalTest
的定义如下:
const PortalTest = () => {
return (
<div className={s.A}>
<div className={s.B}></div>
</div>
);
};
.A {
position: relative;
overflow: hidden;
width: 100px;
height: 100px;
background-color: rgb(228, 228, 228);
}
.B {
position: absolute;
width: 200px;
height: 40px;
background-color: rgb(113, 215, 247);
top: 50%;
transform: translate(0, -50%);
}
A
是一个小盒子,B
是一个小长条,B
的宽度比A
长,但由于A
设置了overflow: hidden
,B
超出的部分将隐藏,如图所示:
真实 DOM 结构如图所示:
解决方式为,使用ReactDOM.createPortal()
,将B
挂载到App
下,跳过A
。
const PortalTest = () => {
const [node, setNode] = useState<ReactPortal>();
useEffect(() => {
const test = <div className={s.B}></div>;
const app = document.getElementById('app') as Element;
setNode(ReactDOM.createPortal(test, app));
}, []);
return <div className={s.A}>{node}</div>;
};
如上代码所示,使用ReactDOM.createPortal(test, app)
渲染B
,挂载到id
为app
的 DOM 节点(App
组件)下。这里使用useEffect
的原因是,要在PortalTest
插入到 DOM 树中,才能渲染子元素。
效果如下,由于B
跳过了A
,直接挂载到App
组件的节点下,所以B
完全展示出来了,不受A
的overflow: hidden
的影响。
真实 DOM 结构如图所示:
冒泡
虽然通过 Portal 可以将子元素挂载到其他的 DOM 节点下,但在其他的任何方面,其行为和普通的 React 子节点行为一致。比如事件冒泡机制,某元素的子元素挂载到其他的 DOM 节点,这个子元素触发的事件,还是会冒泡到该元素上,并不会冒泡至挂载到 DOM 节点。
如下代码所示,有id
分别为A
和B
的两个容器,A
容器下有A
盒子,B
盒子虽然在A
容器里,但被挂载到B
容器中:
const App = () => {
const [node, setNode] = useState<ReactPortal>();
useEffect(() => {
const boxB = <div className={s.box}>B</div>;
const B = document.getElementById('B') as Element;
setNode(ReactDOM.createPortal(boxB, B));
}, []);
const propagationA = () => {
console.log('A');
};
const propagationB = () => {
console.log('B');
};
return (
<>
<div id='A' onClick={propagationA}>
<div className={s.box}>A</div>
{node}
</div>
<div id='B' onClick={propagationB}></div>
</>
);
};
真实的 DOM 结构如图所示,B
盒子确实被挂载到了B
容器中:
A
和B
两容器分别有click
事件,以显示冒泡效果。结果发现,点击B
盒子时,控制台也同样打印'A'
,这说明B
盒子虽然被挂载到B
容器下,但是B
盒子触发的事件,还是会按照原来的机制进行冒泡,会冒泡到A
容器上。
以上是本人学习所得之拙见,若有不妥,欢迎指出交流!
参考:
📘📘欢迎在我的博客上访问:
https://lzxjack.top/