产品需求,实现一个选择器 Selector 组件,要求浮在页面上方。在网上随便找了个图,以下:html
实现这一的一个 Selector 组件并不难,不是本文的讨论内容。
本文讨论的主要是,在有相似于 Selector 组件同样,“浮”在页面的组件时,如何设计 React 组件树?node
方案一:Seletor 组件是 App 组件的子组件。react
优点:Selector 属于 App 的子节点,子节点不受父节点的样式属性( position
overflow
)的干扰。app
劣势:Selector 的显示状态属于 App 节点,跨分支传递状态成本过高。使用 Redux 或 Mobx 跨分支传递状态,依赖第三方组件,不利于复用;而手动传递,至少要 4 个步骤,若是 Button 节点更深,步骤会更多。而且这样写出的代码,耦合性太强,不利于维护。dom
方案二:Selector(fixed) 组件是 Button 组件的子组件。fetch
优点:Selector 的显示状态属于 Button 节点控制,状态管理成本低。ui
劣势:Selector 属于 Button 的子节点。而当父节点 Button 有文字超出隐藏的需求时(overflow: hidden
),子节点 Selector 会被隐藏。this
那么,有没有两全齐美的方案呢?有。spa
方案三:在 React 组件树设计上,Selector 是 Button 的子组件。可是在 DOM 树的角度 Selector 是 Body 的子节点。翻译
在这个方案中,Button 和 Selector 仍是属于 React 组件树中的父子节点,享有父子组件状态传递方便的优点。
可是,Button 和 Selector 再也不属于 DOM 树中的父子节点!Selector 被渲染到了 Body 节点下面,属于 Body 的子节点。这样 Selector 组件不再会受到 Button 组件的样式干扰了。
在 React 中如何作到这一点呢?使用 React 16 的 Portals。
这个新属性的介绍文章很短,我就翻译下一吧。翻译只是意译,只为更好理解。
Portals 提供了一种超级棒的方法,能够将 react 子节点的 DOM 结构,渲染到 react 父节点以外的 DOM 中。
ReactDOM.createPortal(child, container)
第一个参数 child 是任何能够被渲染的 ReactChild,好比 element, string 或者 fragment. 第二个参数 container 是 一个 DOM 元素。
通常来讲,在 react 中是父子节点的关系,那么在 DOM 中也是父子节点的关系。
render() { // 在 react 中 div 和 children 是父子的关系,在 DOM 中 div 和 children 也是父子的关系。 return ( <div> {this.props.children} </div> ); }
然而,有时候打破了这种 react 父子节点和 DOM 父子节点的映射关系是很是有用的。使用 createPortal
能够将 react 的子节点插入到不一样的 DOM 节点中。
render() { // React 并无建立一个新的 div,来包裹 children。它将 children 渲染到了 domNode 中。 // domNode 能够是任意一个合法的 DOM 节点,不管它在 DOM 节点中的哪一个位置。 return ReactDOM.createPortal( this.props.children, domNode, ); }
portal
一个典型的用法是,当父组件有 overflow: hidden
或者 z-index
样式时,可是子组件须要“打破”父组件容器,显示在父组件以外。好比 dialogs,hovercards,tooltips 组件。
[在 CodePen 上尝试一下(https://codepen.io/gaearon/pe...
虽然 portal 能够在 DOM 树中的任意位置,可是它的行为依旧和普通的 React child 同样。好比上下文环境彻底同样,不管 child 是否是 portal; portal 也一直存在于在 React 树上,不管它位于 DOM 树中的什么位置。
包括,事件冒泡。portal 节点的事件会冒泡到它的 React 树的祖先节点上,即便这些 React 树上的祖先节点并非 DOM 树上的祖先节点。好比,有下面的 HTML 结构。
<html> <body> <div id="app-root"></div> <div id="modal-root"></div> </body> </html>
在 DOM 树中是 portal 和它的 React 父组件兄弟节点,可是因为 React 的事件处理规则,让 portal 的 React 父组件有能力捕获 portal 的冒泡事件。
// These two containers are siblings in the DOM const appRoot = document.getElementById('app-root'); const modalRoot = document.getElementById('modal-root'); class Modal extends React.Component { constructor(props) { super(props); this.el = document.createElement('div'); } componentDidMount() { // The portal element is inserted in the DOM tree after // the Modal's children are mounted, meaning that children // will be mounted on a detached DOM node. If a child // component requires to be attached to the DOM tree // immediately when mounted, for example to measure a // DOM node, or uses 'autoFocus' in a descendant, add // state to Modal and only render the children when Modal // is inserted in the DOM tree. modalRoot.appendChild(this.el); } componentWillUnmount() { modalRoot.removeChild(this.el); } render() { return ReactDOM.createPortal( this.props.children, this.el, ); } } class Parent extends React.Component { constructor(props) { super(props); this.state = {clicks: 0}; this.handleClick = this.handleClick.bind(this); } handleClick() { // This will fire when the button in Child is clicked, // updating Parent's state, even though button // is not direct descendant in the DOM. this.setState(prevState => ({ clicks: prevState.clicks + 1 })); } render() { return ( <div onClick={this.handleClick}> <p>Number of clicks: {this.state.clicks}</p> <p> Open up the browser DevTools to observe that the button is not a child of the div with the onClick handler. </p> <Modal> <Child /> </Modal> </div> ); } } function Child() { // The click event on this button will bubble up to parent, // because there is no 'onClick' attribute defined return ( <div className="modal"> <button>Click</button> </div> ); } ReactDOM.render(<Parent />, appRoot);
[在 CodePen 上尝试一下(https://codepen.io/gaearon/pe...
父组件可以捕获 portal 的冒泡事件的设计,容许开发者更加灵活的进行抽象,而这些抽象不依赖于 portal 。例如,若是你渲染一个 <Modal />
组件,它的父组件可以捕获它的事件,不管使用的是否是 portal 实现的 (fixed 也能实现)。
// 数据和选中的元素的状态由 Selector 本身控制 // 不要将 data、index 状态暴露给其余组件 // 暴露给父组件,越多和父组件耦合的就越重 class Selector extends Component { componentDidMount(){ fetch('xxx') .then(data => { this.setState({ data, }) }) } handleSelect = index => { this.setState({ index }) } render() { return ( <List data={this.state.data} index={this.state.index} onSelect={this.handleSelect} /> ) } } // 控制 Modal 显示状态都封装在 Button 中 class Button extends Component { handleClick = () => { this.setState( prevState => ({ show: !prevState.show })) } render() { return ( <div onClick={this.handleClick}> <span>我是按钮</span> // 为了保存 Selector 的状态,不要 unmount Modal,用 display: none 实现隐藏。 <Modal show={this.state.show}> <Selector /> </Modal> </div> ) } } class App extends Component { render() { return ( <div> <Button /> <Other /> </div> ) } }