教你用 React createPortal

14,803 阅读3分钟

Portal 概念介绍

什么是 Portal?

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

在 CSS 中,我们可以使用 position: fixed 等定位方式,让元素从视觉上脱离父元素。在 React 中,Portal 直接改变了组件的挂载方式,不再是挂载到上层父节点上,而是可以让用户指定一个挂载节点。

Portal 实战案例

具体怎么操作呢?我们用一则案例来演示。先创建 index.js 入口文件,引入 App 组件挂载到 root 节点下面:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(<App />, document.getElementById('root'))

我们知道,DOM 树是有层级结构的,就像 HTML 标签的嵌套关系一样,下面是 App 组件的代码:

import { Component } from 'react'
import Dialog from './Dialog'

class App extends Component {
  render() {
    return (
      <>
        <p>我在 root 里面</p>
        <Dialog>
          <p>我不在 root 里面</p>
        </Dialog>
      </>
    )
  }
}

export default App

正常来讲,两个 p 标签最终肯定在 root 节点下面,但是通过 React.createPortal 的方式,能让嵌套在 root 下的 jsx 子元素脱离出去,下面的 Dialog 组件就做了这个事情:

import { Component } from 'react'
import { createPortal } from 'react-dom'

class Dialog extends Component {
  constructor(props) {
    super(props)
    this.dom = document.createElement('div')
    this.dom.setAttribute('id', 'portal')
    document.body.appendChild(this.dom)
  }

  render() {
    return createPortal(<>{this.props.children}</>, this.dom)
  }
}

export default Dialog

可以看到,在 Dialog 组件的构造函数中,在 body 的后面添加了一个 id 为 portal 的 div ,然后在 render 的时候,把其子组件通过 createPortal API 挂载到这个 div 下面,最终渲染出来的效果如下:

react-portal

是不是非常神奇?createPortal 这个 API 通常用于创建模态窗口或对话框之类的场景。

StrictMode 调试彩蛋

到这里,portal 的知识就介绍完了,但是我在写 demo 的过程中,却意外发现了另外一个知识点。开始的时候,我使用 StrictMode 来包裹 App 组件:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

一直不知道 React.StrictMode 这玩意是干啥的,结果就被坑了,因为我在检查元素的时候发现有两个 id 为 portal 的 div,但是我明明只创建了一次啊!

为了验证构造函数到底执行了几次,我加了 console.log 打印,发现控制台确实只打印一次,我就天真地认为真的只执行一次,想了 1 个小时没整明白咋回事,后来在控制台进行断点 debug 时,发现在 StrictMode 下会执行两次!!!

也就是说,下面这句源码里面有坑:

if ( workInProgress.mode & StrictMode) {
  disableLogs();
  try {
    new ctor(props, context);
  } finally {
    reenableLogs();
  }
}

代码可读性非常好,在 StrictMode 把 console.log 给禁用了,然后偷偷的执行了一次构造函数,执行完又把 console.log 给恢复了。为了验证这一点,我们把 Dialog 组件的代码改成:

import { Component } from 'react'
import { createPortal } from 'react-dom'
const print = console.log

class Dialog extends Component {
  constructor(props) {
    super(props)
    console.log('[console.log] dialog constructor')
    print('[print] dialog constructor')
    this.dom = document.createElement('div')
    this.dom.setAttribute('id', 'portal')
    document.body.appendChild(this.dom)
  }

  render() {
    return createPortal(<>{this.props.children}</>, this.dom)
  }
}

export default Dialog

控制台输出如下:

[print] dialog constructor
[console.log] dialog constructor
[print] dialog constructor

后来在官方文档中发现解释: