React Protals

  1. React Protals

React Protals

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

ReactDOM.createPortal(child, container)

注意createPortalReactDOM的api,需要引入react-dom

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的,一个 portal 的典型用例是当父组件有 overflow: hidden z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

下面是一个demo:

import React from 'react'
import ReactDOM from 'react-dom'

// Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
// antd中的Dialog就是用的这个方案

class Modal extends React.Component {
  constructor(props) {
    super(props)
    this.$el = document.createElement('div')
  }
  componentDidMount() {
    document.body.appendChild(this.$el)
  }
  componentWillUnmount() {
    console.log(this.$el)
    document.body.removeChild(this.$el)
  }
  render () {
    return <div>
      {ReactDOM.createPortal(this.props.children, this.$el)}
    </div>
  }
}

class Comp extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      showModal: false
    }
  }

  handleClick = () => {
    this.setState({
      showModal: !this.state.showModal
    })
  }

  render() {
    return <div>
      <button onClick={this.handleClick}>show</button>
      {this.state.showModal ? <Modal>
        <div style={{width: '100px', height: '100px', backgroundColor: 'gray'}}>
          <button onClick={this.handleClick}>hide</button>
        </div>
      </Modal> : null}
    </div>
  }
}

export default Comp

尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树中的祖先。

假设我们在Modal父元素上绑定一个点击事件,则Modal内部的点击会一直冒泡到其React 树的父元素上

import React from 'react'
import ReactDOM from 'react-dom'

class Modal extends React.Component {
  constructor(props) {
    super(props)
    this.$el = document.createElement('div')
  }
  componentDidMount() {
    document.body.appendChild(this.$el)
  }
  componentWillUnmount() {
    console.log(this.$el)
    document.body.removeChild(this.$el)
  }
  render () {
    return ReactDOM.createPortal(this.props.children, this.$el)
  }
}

class Comp extends React.Component {
  constructor(props){
    super(props)
    this.state = {
      showModal: false,
      count: 0
    }
  }

  handleClick = (e) => {
    this.setState({
      showModal: !this.state.showModal
    })
    e.stopPropagation()
  }

  handlePopClick = () => {
    this.setState({
      count: this.state.count + 1
    })
  }

  render() {
    // 在这里绑定事件,每次都会使count +1,
    return <div onClick={this.handlePopClick}>
      <button onClick={this.handleClick}>show</button>
      count: {this.state.count}
      {this.state.showModal ? <Modal>
        <div style={{width: '100px', height: '100px', backgroundColor: 'gray'}}>
          <button onClick={this.handleClick}>hide</button>
        </div>
      </Modal> : null}
    </div>
  }
}

export default Comp

可以看到虽然Modal被渲染到了body上,但是父元素上还是能捕获到冒泡上来的事件。

可以理解为Portals只是把dom渲染到了其他地方,但是从代码结构上来看,还是处在当前父子组件的上下文中


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com