Portals

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

  1. ReactDOM.createPortal(child, container)

第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

用法

通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点:

  1. render() {
  2. // React 挂载了一个新的 div,并且把子元素渲染其中
  3. return (
  4. <div>
  5. {this.props.children}
  6. </div>
  7. );
  8. }

然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的:

  1. render() {
  2. // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。
  3. // `domNode` 是一个可以在任何位置的有效 DOM 节点。
  4. return ReactDOM.createPortal(
  5. this.props.children,
  6. domNode
  7. );
  8. }

一个 portal 的典型用例是当父组件有 overflow: hiddenz-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:

注意:

当在使用 portal 时, 记住管理键盘焦点就变得尤为重要。

对于模态对话框,通过遵循 WAI-ARIA 模态开发实践,来确保每个人都能够运用它。

在 CodePen 上尝试

通过 Portal 进行事件冒泡

尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。

这包含事件冒泡。一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。假设存在如下 HTML 结构:

  1. <html>
  2. <body>
  3. <div id="app-root"></div>
  4. <div id="modal-root"></div>
  5. </body>
  6. </html>

#app-root 里的 Parent 组件能够捕获到未被捕获的从兄弟节点 #modal-root 冒泡上来的事件。

  1. // 在 DOM 中有两个容器是兄弟级 (siblings)
  2. const appRoot = document.getElementById('app-root');
  3. const modalRoot = document.getElementById('modal-root');
  4. class Modal extends React.Component {
  5. constructor(props) {
  6. super(props);
  7. this.el = document.createElement('div');
  8. }
  9. componentDidMount() {
  10. // 在 Modal 的所有子元素被挂载后,
  11. // 这个 portal 元素会被嵌入到 DOM 树中,
  12. // 这意味着子元素将被挂载到一个分离的 DOM 节点中。
  13. // 如果要求子组件在挂载时可以立刻接入 DOM 树,
  14. // 例如衡量一个 DOM 节点,
  15. // 或者在后代节点中使用 ‘autoFocus’,
  16. // 则需添加 state 到 Modal 中,
  17. // 仅当 Modal 被插入 DOM 树中才能渲染子元素。
  18. modalRoot.appendChild(this.el);
  19. }
  20. componentWillUnmount() {
  21. modalRoot.removeChild(this.el);
  22. }
  23. render() {
  24. return ReactDOM.createPortal(
  25. this.props.children,
  26. this.el,
  27. );
  28. }
  29. }
  30. class Parent extends React.Component {
  31. constructor(props) {
  32. super(props);
  33. this.state = {clicks: 0};
  34. this.handleClick = this.handleClick.bind(this);
  35. }
  36. handleClick() {
  37. // 当子元素里的按钮被点击时,
  38. // 这个将会被触发更新父元素的 state,
  39. // 即使这个按钮在 DOM 中不是直接关联的后代
  40. this.setState(state => ({
  41. clicks: state.clicks + 1
  42. }));
  43. }
  44. render() {
  45. return (
  46. <div onClick={this.handleClick}>
  47. <p>Number of clicks: {this.state.clicks}</p>
  48. <p>
  49. Open up the browser DevTools
  50. to observe that the button
  51. is not a child of the div
  52. with the onClick handler.
  53. </p>
  54. <Modal>
  55. <Child />
  56. </Modal>
  57. </div>
  58. );
  59. }
  60. }
  61. function Child() {
  62. // 这个按钮的点击事件会冒泡到父元素
  63. // 因为这里没有定义 'onClick' 属性
  64. return (
  65. <div className="modal">
  66. <button>Click</button>
  67. </div>
  68. );
  69. }
  70. ReactDOM.render(<Parent />, appRoot);

在 CodePen 上尝试

在父组件里捕获一个来自 portal 冒泡上来的事件,使之能够在开发时具有不完全依赖于 portal 的更为灵活的抽象。例如,如果你在渲染一个 <Modal /> 组件,无论其是否采用 portal 实现,父组件都能够捕获其事件。