在设计 UI 组件的过程中不可避免的需要考虑模态窗的需求, 比如 dialog,tooltip 这些, 但是在 React 的框架下, 我们似乎遇到了一些问题
React 下的 modal 需求
通常在设计这些模态窗的时候, 会把整个 DOM 结构尽量渲染在 html 位置比较顶层的地方, 比如 body 这样相对来说样式的自由度会比较高 但是在 React 的整体框架下, 它的数据流向是自上而下的, 如果你的 modal 中的内容依赖父级的数据, 那可能就要将对应的组建挂载在依赖组建里面当然可以在顶层组件管理 modal 数据或者直接上 Redux, 但是这对于一个定位成 UI 组件的设计上来说, 显示不够合理这便促成了 portal 的想法出现 --- 希望 modal 组件 能跟正常的组件一样不管哪里需要就在哪里挂载, 但实际 DOM 的位置确是另外一个地方 (比如 React Bootstrap 的 Portal 实现)
React16 之前的实现思路
首先组件不能渲染在它挂载的地方
- render() {
- return null;
- }
DOM 真正渲染的位置, 通过 renderLayer 来实现
- renderLayer() {
- // 这里我们假定 render 的执行是输出渲染内容
- const { render } = this.props;
- // 构造 DOM 节点作为渲染内容的容器
- if (!this.layer) {
- this.layer = document.createElement('div');
- document.body.appendChild(this.layer);
- }
- const layerElement = render();
- this.layerElement = ReactDOM.unstable_renderSubtreeIntoContainer(this, layerElement, this.layer);
- }
- unrenderLayer() {
- if (this.layer) {
- React.unmountComponentAtNode(this.layer);
- document.body.removeChild(this.layer);
- this.layer = null;
- }
- }
好了, 我们在各个生命周期里面调用它们就行了
ReactDOM 中提供了一个 unstable_renderSubtreeIntoContainer, 从名字上就可以发现, 它并不推荐被使用, 实际上它也的确表现得令人费解
- class Test extends React.Component {
- componentDidMount() {
- console.log('test');
- setTimeout(() => this.forceUpdate(),5000)
- }
- componentDidUpdate() {
- console.log('did update test')
- }
- render() {
- return <p>test<A/></p>;
- }
- }
- class B extends React.Component {
- componentDidMount() {
- console.log('did mount B')
- }
- componentDidUpdate() {
- console.log('did update B')
- }
- render() {
- return <a>some thing</a>;
- }
- }
- class A extends React.Component {
- componentDidMount() {
- this.renderLayer();
- console.log('did mount A')
- }
- componentDidUpdate() {
- this.renderLayer();
- console.log('did update A')
- }
- renderLayer() {
- if (!this.layer) {
- this.layer = document.createElement('div');
- document.body.appendChild(this.layer);
- }
- ReactDOM.unstable_renderSubtreeIntoContainer(this, <B/>, this.layer);
- }
- render() {
- return null;
- }
- }
- ReactDOM.render(<Test />, document.getElementById('app'));
https://codepen.io/anon/pen/GQRaEo?editors=1112 按我们对 React 父子组件间生命周期的执行情况上理解应当输出
- "did mount B"
- "did mount A"
- "test"
- "did update B"
- "did update A"
- "did update test"
而实际的结果却是
- "did mount B"
- "did mount A"
- "test"
- "did update A"
- "did update test"
- "did update B"
显然在初始化的时候事情还是符合我们预期的 可是在执行更新组件的时候, 生命周期的执行便显得很混乱, 在 React16 的版本中这个问题得到了修复, 但执行的结果显然也不是我们最终想要的 https://codepen.io/anon/pen/MQWdPq?editors=1111
- "did mount A"
- "test"
- "did mount B"
- "did update A"
- "did update test"
- "did update B"
React Portal 的出现彻底解决了这方面的问题
React Portal
终于进入主题, 先看看它是如何使用的
- const node = document.createElement('div');
- document.body.appendChild(this.node);
- ...
- render() {
- return createPortal(
- <div class="dialog">
- {this.props.children}
- </div>, // 需要渲染的内容
- node // 渲染内容的容器 DOM
- );
- }
除了 node 节点在一些场景下需要释放之外, 你已经不需要在其他生命周期里面擦屁股了 让我们在回到之前生命周期执行上的问题 https://codepen.io/anon/pen/Jpjqwg?editors=1111 结果的执行跟我们正常组件保持了一致, 再也不用担心一些依赖子组件完成更新后的监听或操作会出现异常情况了 除此之外 React Portal 还新增了一个事件冒泡的实现
- <div onClick={handleClick}>
- <Dialog/>
- </div>
如果在 React16 之前的实现方式, 点击 Dialog 组件里面的内容 handleClick 是不会被触发, 但通过 React Portal 实现的挂载方式将会发生冒泡 这个特性见仁见智吧, 一般情况下感觉也不会用到
总结
总之 React Portal 的实现对于 modal 的实现是一个重大的更新, 同时也避免了组件间生命周期的执行混乱
来源: https://juejin.im/post/5aa777e351882555867f15be