通常情况下,我们的组件结构如下:
<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> <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 节点。
使用:
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>
|
在上面的例子,modal 能够沿着其组件层次进行事件冒泡,可以很灵活的处理事件。
来自 React 官方文档的可运行示例:Example: Portal event bubbling
兼容 React16 以下的版本
在 React16 之前版本提供一个不稳定的 API: ReactDOM.unstable_renderSubtreeIntoContainer
。 react-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 { componentDidMount() { if (!isReact16) { this.node = document.createElement("div"); } this.node.className = 'portalClassName'; document.body..appendChild(this.node); !isReact16 && this.renderPortal(this.props); } componentWillReceiveProps(newProps) { !isReact16 && this.renderPortal(newProps); }
componentWillUnmount() { this.removePortal(); } renderPortal(props) { const portal = ReactDOM.unstable_renderSubtreeIntoContainer( this, <Modal {...props} />, this.node ); this.portal = portal; }
removePortal = () => { if (!this.node || !this.portal) return; !isReact16 && ReactDOM.unmountComponentAtNode(this.node); document.body.removeChild(this.node); };
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
前缀的接口都不稳定,不鼓励使用,如果有相关的需求,还是直接使用封装好的类库较好。
参考: