默认情况,组件的 render
方法返回一个元素时,会被挂在到最近的 DOM 节点上,也就是其父节点。比如这样
Item 组件会被挂载在 className
为 “wrap
” 的 div
节点上,Item 返回的内容会被渲染在 App 组件渲染的区域内。
但是有时候我们希望在父组件内使用子组件,但是在子组件的渲染内容不会出现在该父组件渲染区域内,而是出现在别的地方,甚至挂载的DOM节点并不在该父组件的子节点中。
如下图的需求:
- Button 区是一个组件,其内容根据导航的不同显示内容不同;
- Button 组件的渲染结果还受内容区中当前活跃Tab(基本信息、部署配置、权限分配)页的不同而不同
- Button 区组件和内容区组件不是父子组件关系,而是兄弟节点的关系
不使用传送门的实现
实现的关键是:每个 Tab 页都单独定义一个 Button 区域组件,通过 CSS 绝对定位定位到指定位置。
这样做可以保证 Button 区域的按钮随着内容区的Tab也切换而改变,因为它们本身就是挂载在下面的Tab页的DOM节点上的。
但是,由于是通过绝对定位将渲染的视觉位置改变,所以需要梳理好父节点及其兄弟节点间的样式关系。比如内容区不能设置 overflow: hidden;
样式;还有在 Tab 组件到 button 区之间可能有其他 position: relative
(或 absolute
)元素的影响等等。
使用传送门
React Portal 用法
child
是被传送过去要渲染的内容,是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment
。
React 不会创建一个新的 div。它只是把第一个参数的子元素渲染到“container
”中。“container
” 是一个可以在任何位置的有效 DOM 节点。就如同官方例子中所示
- 先通过
document.createElement
创建一个没有挂载在任何地方的div
元素 - 将这个
div
元素通过appendChild
方法添加到指定 DOM 节点下 - 再将这个
div
元素作为createPortal
的第二个参数,其实就是将子元素渲染进这个div
中,那么也就将想要渲染的内容渲染在指定位置了。
先看一个官方例子:
PS: 这里有个疑问,为什么不直接将目标DOM节点当做第二个参数传进去?
实际测试,直接传目标 DOM 节点是完全可以的(看这里,Fork的官方例子修改)。但问题是,就如同描述的那样第二个参数可以在任何位置。也就是说我们无法保证传进去的DOM节点已经被渲染,所以要手动加一些校验,防止出现“当传送时目标DOM还没有被渲染”的情况。
有了基础知识,那么大概思路就有了:
- 先搭建好 Button 区域和 Content 区域的DOM结构
- 创建一个通用的 ButtonPortal 组件,通过 props.children 接受需要渲染的内容,然后使用传送门发送并挂载到 Buttons 区域中用来占位的DOM元素上
- 在 Content 组件内有三个 Tab,每个Tab Panel都是一个单独的组件,不同的 Panel 调用 ButtonPortal 组件 ,并将渲染的按钮信息通过props传递给 ButtonPortal
思路有了,根据思路的大致代码结构也就可以写出来了(在线效果)
通过 Portal 进行事件冒泡
官方文档的示例
虽然 portal 可以被放置在 DOM 树中的任何地方,但是其行为和普通的 React 子节点行为一致。比如 context
功能、事件冒泡等等。
拿事件冒泡来说,(React v16 之后)在 portal 渲染的 DOM 内部触发的事件会一直冒泡到开启传送的源位置(不是实际渲染挂载的DOM位置)。比如官方文档的示例中,在 #app-root
里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root
冒泡上来的事件。
为什么 React 需要传送门?
React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render
到一个组件里面去,实际改变的是网页上另一处的DOM结构。
比如,某个组件在渲染时,在某种条件下需要显示一个对话框(Dialog),这该怎么做呢?
而 portal 的典型用例就是当父组件有 overflow: hidden
或 z-index
样式时,但需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
React在v16之前的传送门实现方法
在v16之前,实现“传送门”,要用到两个秘而不宣的React API
unstable_renderSubtreeIntoContainer
unmountComponentAtNode
第一个 unstable_renderSubtreeIntoContainer
,都带上前缀 unstable
了,就知道并不鼓励使用,但是没办法,不用也得用,还好 React 一直没有 deprecate 这个 API,一直挺到 v16 直接支持 portal。这个API的作用就是建立“传送门”,可以把JSX代表的组件结构塞到传送门里面去,让他们在传送门的另一端渲染出来。
第二个 unmountComponentAtNode
用来清理第一个 API 的副作用,通常在 unmount
的时候调用,不调用的话会造成资源泄露的。
一个通用的Dialog组件的实现差不多是这样,注意看 renderPortal
中的注释。
- 首先,
render
函数不要返回有意义的JSX
,也就说说这个组件通过正常生命周期什么都不画,要是画了,那画出来的 HTML/DOM 就直接出现在使用 Dialog 的位置了,这不是我们想要的。 - 在
componentDidMount
里面,利用原生 API 来在body
上创建一个div
,这个div
的样式绝对不会被其他元素的样式干扰。 - 然后,无论
componentDidMount
还是componentDidUpdate
,都调用一个renderPortal
来往“传送门”里塞东西。
总结,这个Dialog组件做得事情是这样:
- 它什么都不给自己画,
render
返回一个null
就够了; - 它做得事情是通过调用
renderPortal
把要画的东西画在DOM树上另一个角落。
参考
- Portals
- 传送门:React Portal ——程墨
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!