• Portals
    • 用法
    • 通过 Portal 进行事件冒泡

    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 实现,父组件都能够捕获其事件。