教你用 React createPortal
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 下面,最终渲染出来的效果如下:
是不是非常神奇?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
后来在官方文档中发现解释:
转载自:https://juejin.cn/post/7036380015365193735