React Portal
React Portal
参考链接:传送门:React Portal
官方网站:Portals-React
Portal – 传送门
为什么React需要传送门?
React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。
在React中,一切皆为组件,用组件可以表示一切界面中发生的逻辑,不过,有些特例处理起来还比较麻烦,比如,某个组件在渲染时,在某种条件下需要显示一个对话框(Dialog),这该怎么做呢?
最直观的做法,就是直接在JSX中把Dialog画出来,像下面代码的样子。
1 |
|
问题是,我们写一个Dialog组件,就这么渲染的话,Dialog最终渲染产生的HTML就存在于上面JSX产生的HTML一起了,类似下面这样。
1 |
|
可是问题来了,对话框应该是一个独立的组件,通常应该显示在屏幕的最中间,现在Dialog被包在其他组件中,要用CSS的position属性控制Dialog位置,就要求从Dialog往上一直到body没有其他postion是relative的元素干扰,这有点难为作为通用组件的Dialog,毕竟,谁管得住所有组件不用position呢。还有一点,Dialog的样式,因为包在其他元素中,各种样式纠缠,CSS样式太容易搞成一坨浆糊了。
因此,React 就引入了 Portal 传送门的概念。
React v16 之前的传送门实现方法
为什么要讲旧版本的实现方法呢?因为旧版本更能体现传送门实现的一个思想,而新版本更多的是一个封装和便于使用,理解了旧版本就可以更好地使用新版本 Portal 了。
在v16之前,实现“传送门”,要用到两个秘而不宣的React API
1 |
|
- 第一个unstable_renderSubtreeIntoContainer。这个API的作用就是建立“传送门”,可以把JSX代表的组件结构塞到传送门里面去,让他们在传送门的另一端渲染出来。
- 第二个unmountComponentAtNode用来清理第一个API的副作用,通常在unmount的时候调用,不调用的话会造成资源泄露的。
一个通用的Dialog组件的实现差不多是这样,注意看renderPortal中的注释。
1 |
|
- 首先,**
render
函数不要返回有意义的JSX
(即返回null
)**,也就说说这个组件通过正常生命周期什么都不画,要是画了,那画出来的HTML/DOM就直接出现在使用Dialog的位置了,这不是我们想要的。 - 在**
componentDidMount
里面,利用原生API来在body上创建一个div**,这个div的样式绝对不会被其他元素的样式干扰。 - 然后,无论
componentDidMount
还是componentDidUpdate
,都调用一个renderPortal
来往“传送门”里塞东西。 - 在
renderPortal
中,利用unstable_renderSubtreeIntoContainer
函数往直前创建的div
里塞JSX
,这里我们用的JSX
是这样。1
2
3
4
5
6
7
8
9
10<div class="dialog">
{props.children}
</div>
//--------------------
//调用 Dialog 组件时,可以加上任意的子组件。
<Dialog>
What ever shit
<div>Hello</div>
<p>World</p>
</Dialog>
总结,这个Dialog组件做得事情是这样:
- 它什么都不给自己画,render返回一个null就够了;
- 它做得事情是通过调用renderPortal把要画的东西画在DOM树上另一个角落。
React v16 Portal
正因为 Portal 的强大能力,React v16 开始正式支持 Portal。
在v16中,使用Portal创建Dialog组件简单多了,不需要牵扯到componentDidMount、componentDidUpdate,也不用调用API清理Portal,关键代码在render中,像下面这样就行。
1 |
|
整体思想是类似的:
- 在
constructor
中,获取 DOM,用原生 API 创建节点。 - 将该节点加载到 DOM 文档树的 body 部分。
- 调用
createPortal(child,container)
方法创建新的 JSX 元素(即构造新组件)。**createPortal
方法接收两个参数,child
是任何可渲染的React子元素,例如元素,字符串或片段。container
是将被传送到的目标节点(DOM元素),它会将child
插入container
中,并且将child
传送到container
元素内的最底部。** - 在组件销毁时调用
componentWillUnmout
将 body 中的节点<div></div>
移除。 - 在写模态框时,用了portal,就不会完全挡死,只需调节z-Index,可以覆盖页面上的任意元素(存疑,video,canvas等这类元素未试过)。
事件冒泡
v16之前的React Portal实现方法,有一个小小的缺陷,就是Portal是单向的,内容通过Portal传到另一个出口,在那个出口DOM上发生的事件是不会冒泡传送回进入那一端的。具体详情可以看官方文档,有详细的说明。
代码如下:
1 |
|
在Dialog画出的内容上点击,onDialogClick是不会被触发的。
在v16中,通过Portal渲染出去的DOM,事件是会冒泡从传送门的入口端冒出来的,上面的onDialogClick也就会被调用到了。
实战
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!