超级面板
文章目录
最新文章
最近更新
文章分类
标签列表
文章归档

React - Protals

通常情况下,我们的组件结构如下:

<div id="app">
<RootComponent>
<ParentComponent>
<ModalComponent>
// modal text
</ModalComponent>
</ParentComponent>
<ParentSblingsComponent>
// come text
</ParentSblingsComponent>
</RootComponent>
</div>

对应的 html 结构如下:

<div id="appRoot">
<div class="root">
<div class="parent">
<div class="modal">
// modal text
</div>
</div>
<div class="parent-sblings">
// modal text
</div>
</div>
</div>

这种情况在大多数渲染情况下并不会出现问题,但是当出现 z-index 的时候:

.parent {
z-index: 1
}
.parent-sblings {
z-index: 2
}

这个时候 modal 将无法出现在所有的组件上层,因为其父元素(ParentComponent)的 z-index 小于其父元素的兄弟元素(ParentSblingsComponent),这样的话就出现了问题, modal 会被 ParentSblingsComponent 的内容覆盖。

除非我们让 ModalComponent 的渲染位置脱离其文档的组件结构:


<div id="appRoot">
<div class="root">
<div class="parent">
<!--
<div class="modal">
// modal text
</div>
-->
</div>
<div class="parent-sblings">
// modal text
</div>
</div>
</div>
<div id="modalRoot">
<div class="modal">
// modal text
</div>
</div>

也就是说让 ModalComponent 的渲染位置位于文档的最后面,这样就不会出现被覆盖的问题。而 Portals 的引入,正是为了解决这样的问题:

Portals 简介

Portals 是 React 16 以后引入的新的功能,用于将组件渲染到父元素节点以外的 DOM 节点中,多用于各种弹出模态框、下拉列表等内容。

ReactDOM.createPortal(child, container)

  • child:刻意渲染的 React 内容,比如 react 元素、字符串、fragment、DOM 元素等;
  • container:渲染的目标 DOM 节点。

使用:

// modal render
class Modal extends React.Component {
render() {
return ReactDOM.createPortal(
this.props.children,
document.getElementById('modalRoot')
)
}
}

Portals 事件冒泡

尽管使用 Portals 渲染的元素在 DOM 中的位置脱离了 ParentComponent 元素的 DOM 树结构,但是,使用 Portals 渲染的元素仍然会触发到其组件结构层次上的事件冒泡:

<ParentComponent onClick={() => console.log('click bubbled!')}>
<ModalComponent>
// modal text
</ModalComponent>
</ParentComponent>

// click modal
// log: click bubbled!

在上面的例子,modal 能够沿着其组件层次进行事件冒泡,可以很灵活的处理事件。

来自 React 官方文档的可运行示例:Example: Portal event bubbling

兼容 React16 以下的版本

在 React16 之前版本提供一个不稳定的 API: ReactDOM.unstable_renderSubtreeIntoContainerreact-modal 组件就是使用这种方式进行兼容的:

const createPortal = isReact16
? ReactDOM.createPortal
: ReactDOM.unstable_renderSubtreeIntoContainer;

ReactDOM.unstable_renderSubtreeIntoContainer 这个接口可以用于代替 ReactDOM.createPortal 的功能,但是使用它以后,需要使用 ReactDOM.unmountComponentAtNode 清理副作用。

这里我们可以看一下如何使用它(代码来自 react-modal,大量删减和本节无关的内容,完整代码情请点击链接查看:react-modal/src/components/Modal.js):

class OurModal extends Component {
// modal 在 componentDidMount 中调用 renderPortal 执行初始化加载。
componentDidMount() {
if (!isReact16) {
this.node = document.createElement("div");
}
this.node.className = 'portalClassName';
document.body..appendChild(this.node);
!isReact16 && this.renderPortal(this.props);
}
// modal 在 componentWillReceiveProps 中调用 renderPortal 执行更新渲染。
componentWillReceiveProps(newProps) {
!isReact16 && this.renderPortal(newProps);
}

// 组件移除需要执行的一些清理工作
componentWillUnmount() {
this.removePortal();
}
// renderPortal 就是使用 unstable_renderSubtreeIntoContainer,
// 将 modal 的内容更新到指定的 DOM 树上(本例是 document.body)
renderPortal(props) {
const portal = ReactDOM.unstable_renderSubtreeIntoContainer(
this,
<Modal {...props} />,
this.node
);
this.portal = portal;
}

// 使用了 unstable_renderSubtreeIntoContainer 就需要调用 unmountComponentAtNode 进行清理
// unmountComponentAtNode 函数将 React 组件从 DOM 中清理,并清除它的事件处理器
// 然后将 modal 内容从 document.body 中删除
removePortal = () => {
if (!this.node || !this.portal) return;
!isReact16 && ReactDOM.unmountComponentAtNode(this.node);
document.body.removeChild(this.node);
};

// React16 以下 render 方法必须返回 null
// React16 直接使用 createPortal
render() {
if (!isReact16) {
return null;
}

return createPortal(...);
}
}

从上面示例可以看出,使用替换 Api 的步骤较为繁琐,需要维护额外的组件的挂载,更新和清理工作,而且使用 unstable_renderSubtreeIntoContainer,发生的事件不会从 modal 的 DOM 冒泡到 Modal 组件上:

<ParentComponent onClick={() => console.log('click will not bubble!')}>
<OurModal>
// modal text
</OurModal>
</ParentComponent>

同时带有 unstable 前缀的接口都不稳定,不鼓励使用,如果有相关的需求,还是直接使用封装好的类库较好。

参考: