React v16 引入了一项名为 portals的新功能。
门户提供了一种第一类的方式来将孩子转化为存在于父母组件的DOM层次外的DOM节点。
通常情况下,功能组件或类组件会渲染一个 React 元素树(通常由 JSX 生成)。
在 v16 之前,只有少数儿童类型被允许渲染:
null
或false
(表示无处不在)。- JSX.
- 反应元素
1function Example(props) {
2 return null;
3}
4function Example(props) {
5 return false;
6}
7function Example(props) {
8 return <p>Some JSX</p>;
9}
10function Example(props) {
11 return React.createElement(
12 'p',
13 null,
14 'Hand coded'
15 );
16}
在v16中,更多的儿童类型可以渲染:
- 數字(包括「無限」和「NaN」)。
- 字符串
- 回應端口
- 可渲染的兒童數量
眾生,眾生,眾生,眾生,眾生,眾生。
1function Example(props) {
2 return 42; // Becomes a text node.
3}
4function Example(props) {
5 return 'The meaning of life.'; // Becomes a text node.
6}
7function Example(props) {
8 return ReactDOM.createPortal(
9 // Any valid React child type
10 [
11 'A string',
12 <p>Some JSX</p>,
13 'etc'
14 ],
15 props.someDomNode
16 );
17}
React 门户是通过调用 ReactDOM.createPortal
来创建的。第一个参数应该是可渲染的孩子。第二个参数应该是可渲染的孩子将被渲染的 DOM 节点的参考。
请注意,createPortal 位于 ReactDOM 名称空间中,而不是 CreateElement 这样的 React 名称空间。
一些观察读者可能注意到ReactDOM.createPortal
的签名与ReactDOM.render
相同,这使其易于记住。
何时使用
React 门户非常有用,当一个主组件具有过流:隐藏
的声明或具有影响 堆栈背景的属性,并且您需要视觉地打破
其容器。
事件泡沫通过门户
React 文件非常清楚地解释了这一点。
尽管一个门户可以在DOM树的任何地方,但它在所有其他方面都表现得像一个正常的React孩子一样。 背景功能完全相同,无论孩子是否是一个门户,因为门户仍然存在于React树中,不管它在DOM树中的位置
这包括事件泡沫。
这使得在您的对话、浮动卡等中聆听事件,就像它们在与母组件相同的DOM树中进行渲染一样容易。
例子
在下面的示例中,我们将利用 React 门户和其事件泡沫功能。
标记开始如下。
1<div class="PageHolder">
2</div>
3<div class="DialogHolder is-empty">
4 <div class="Backdrop"></div>
5</div>
6<div class="MessageHolder">
7</div>
.PageHolder
div
是我们应用程序的主要部分生活的地方。 .DialogHolder
div
将是任何生成的对话进行渲染的地方。
由于我们希望所有对话都视觉上位于我们的应用程序的主要部分,所以.DialogHolder``div
具有z-index: 1
的声明,这将创建一个新的堆积环境,而不论是.PageHolder
的堆积环境。
因为我们希望所有的消息都视觉上超过任何对话,所以.MessageHolder``div
有z-index: 1
的声明,这将为.DialogHolder
的堆积背景创建一个兄弟的堆积背景,尽管兄弟堆积背景的z-index
具有相同的价值,但这仍然会反映出我们想要的样子,因为在DOM树中.MessageHolder
之后。
下面的 CSS 概述了建立所需的堆栈背景所需的规则。
1.PageHolder {
2 /* Just use stacking context of parent element. */
3 /* A z-index: 1 would still work here. */
4}
5
6.DialogHolder {
7 position: fixed;
8 top: 0; left: 0;
9 right: 0; bottom: 0;
10 z-index: 1;
11}
12
13.MessageHolder {
14 position: fixed;
15 top: 0; left: 0;
16 width: 100%;
17 z-index: 1;
18}
该示例将有一个页面
组件,该组件将转换为.PageHolder
。
1class Page extends React.Component { /* ... */ }
2
3ReactDOM.render(
4 <Page/>,
5 document.querySelector('.PageHolder')
6)
由于我们的页面
组件将将对话和消息渲染到.DialogHolder
和.MessageHolder
,分别,它将需要在渲染时引用这些持有人div
。
我们可以在渲染页面
组件之前解决这些持有者div
的引用,并将它们作为属性传递给页面
组件。
1let dialogHolder = document.querySelector('.DialogHolder');
2let messageHolder = document.querySelector('.MessageHolder');
3
4ReactDOM.render(
5 <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>,
6 document.querySelector('.PageHolder')
7);
我们可以将选项转移到页面
组件作为属性,然后在组件WillMount
中解决初始渲染的引用,如果选项改变,则在组件WillReceiveProps
中重新解决。
1class Page extends React.Component {
2
3 constructor(props) {
4 super(props);
5 let { dialogHolder = '.DialogHolder',
6 messageHolder = '.MessageHolder' } = props
7
8 this.state = {
9 dialogHolder,
10 messageHolder,
11 }
12 }
13
14 componentWillMount() {
15 let state = this.state,
16 dialogHolder = state.dialogHolder,
17 messageHolder = state.messageHolder
18
19 this._resolvePortalRoots(dialogHolder, messageHolder);
20 }
21
22 componentWillReceiveProps(nextProps) {
23 let props = this.props,
24 dialogHolder = nextProps.dialogHolder,
25 messageHolder = nextProps.messageHolder
26
27 if (props.dialogHolder !== dialogHolder ||
28 props.messageHolder !== messageHolder
29 ) {
30 this._resolvePortalRoots(dialogHolder, messageHolder);
31 }
32 }
33
34 _resolvePortalRoots(dialogHolder, messageHolder) {
35 if (typeof dialogHolder === 'string') {
36 dialogHolder = document.querySelector(dialogHolder)
37 }
38 if (typeof messageHolder === 'string') {
39 messageHolder = document.querySelector(messageHolder)
40 }
41 this.setState({
42 dialogHolder,
43 messageHolder,
44 })
45 }
46
47}
现在我们已经确保我们将为门户提供DOM引用,我们可以将页面组件转换为对话和消息。
与 React 元素一样,React 门户基于组件属性和状态进行渲染。对于这个例子,我们将有两个按钮。一个将创建对话门户,在点击时在对话持有器中进行渲染,另一个将创建消息门户,在消息持有器中进行渲染。
1class Page extends React.Component {
2 // ...
3
4 constructor(props) {
5 super(props);
6 let { dialogHolder = '.DialogHolder',
7 messageHolder = '.MessageHolder' } = props
8
9 this.state = {
10 dialogHolder,
11 dialogs: [],
12 messageHolder,
13 messages: [],
14 }
15 }
16
17 render() {
18 let state = this.state,
19 dialogs = state.dialogs,
20 messages = state.messages
21
22 return (
23 <div className="Page">
24 <button onClick={evt => this.addNewDialog()}>
25 Add Dialog
26 </button>
27 <button onClick={evt => this.addNewMessage()}>
28 Add Message
29 </button>
30 {dialogs}
31 {messages}
32 </div>
33 )
34 }
35
36 addNewDialog() {
37 let dialog = ReactDOM.createPortal((
38 <div className="Dialog">
39 ...
40 </div>
41 ),
42 this.state.dialogHolder
43 )
44 this.setState({
45 dialogs: this.state.dialogs.concat(dialog),
46 })
47 }
48
49 addNewMessage() {
50 let message = ReactDOM.createPortal((
51 <div className="Message">
52 ...
53 </div>
54 ),
55 this.state.messageHolder
56 )
57 this.setState({
58 messages: this.state.messages.concat(message),
59 })
60 }
61
62 // ...
63}
为了证明事件会从 React 门户组件到母组件的泡沫,让我们在 .Page
div
上添加一个点击处理器。
1class Page extends React.Component {
2 // ...
3
4 render() {
5 let state = this.state,
6 dialogs = state.dialogs,
7 messages = state.messages
8
9 return (
10 <div className="Page" onClick={evt => this.onPageClick(evt)}>
11 ...
12 </div>
13 )
14 }
15
16 onPageClick(evt) {
17 console.log(`${evt.target.className} was clicked!`);
18 }
19
20 // ...
21}
当单击对话或消息时,将呼叫onPageClick
事件处理器(只要另一个处理器没有停止传播)。
看一看,一看,一看,一看,一看,一看。
👉在遇到过流:隐藏
或堆积背景问题时使用 React 门户!