Skip to content

组件设计模式(5):组合组件

作者:Long Mo
字数统计:1.7k 字
阅读时长:5 分钟

这一小节我们介绍“组合组件”(Compound Component)这种模式。 网上介绍这种模式的文章也不少,但是都是一上来直接讲实现细节,却很少说应用于什么场景,所以往往都讲得让读者云里雾里。

这里我要强调,所谓模式,就是特定于一种问题场景的解决办法。

text
模式(Pattern) = 问题场景(Context) + 解决办法(Solution)

如果不搞清楚场景,单纯知道有这么一个办法,就好比拿到了一杆枪却不知道这杆枪用于打什么目标,是没有任何意义的。

并不是所有的枪都是一样的,有的枪擅长狙击,有的枪适合近战,有的枪只是发个信号。

模式就是我们的武器,我们一定要搞清楚一件武器应用的场合,才能真正发挥这件武器的威力。

组合组件模式要解决的是这样一类问题:父组件想要传递一些信息给子组件,但是,如果用 props 传递又显得十分麻烦。

一看到这个问题描述,读者应该能立刻想到上一节我们介绍过的 Context API,利用 Context,可以让组件之间不用 props 来传递信息。

不过,使用 Context 也不是完美解法,上一节我们介绍过,使用 React 在 v16.3.0 之后提供的新的 Context API, 需要让“提供者”和“消费者”共同依赖于一个 Context 对象,而且消费者也要使用 render props 模式。

如果不嫌麻烦,用 Context 来解决问题当然好,但是我们肯定会想有没有更简洁的方式。

问题描述

为了让问题更加具体,我们来解决一个实例。

很多界面都有 Tab 这样的元件,我们需要一个 Tabs 组件和 TabItem 组件,Tabs 是容器,TabItem 是一个一个单独的 Tab,因为一个时刻只有一个 TabItem 被选中,很自然希望被选中的 TabItem 样式会和其他 TabItem 不同。

这并不是一个很难的功能,首先我们想到的就是,用 Tabs 中一个 state 记录当前被选中的 TabItem 序号,然后根据这个 state 传递 props 给 TabItem,当然,还要传递一个 onClick 事件进去,捕获点击选择事件。

按照这样的设计,Tabs 中如果要显示 One、Two、Three 三个 TabItem,JSX 代码大致这么写:

jsx
<TabItem active={true} onClick={this.onClick}>One</TabItem>
<TabItem active={false} onClick={this.onClick}>Two</TabItem>
<TabItem active={false} onClick={this.onClick}>Three</TabItem>

上面的 TabItem 组件接受 active 这个 props,如果 true 代表当前是选中状态,当然可以工作,但是,也存在大问题:

  • 每次使用 TabItem 都要传递一堆 props,好麻烦;
  • 每增加一个新的 TabItem,都要增加对应的 props,更麻烦;
  • 如果要增加 TabItem,就要去修改 Tabs 的 JSX 代码,超麻烦。

我们不想要这么麻烦,理想情况下,我们希望可以随意增加减少 TabItem 实例,不用传递一堆 props,也不用去修改 Tabs 的代码,最好代码就这样:

jsx
<Tabs>
	<TabItem>One</TabItem>
	<TabItem>Two</TabItem>
	<TabItem>Three</TabItem>
</Tabs>

如果能像上面一样写代码,那就达到目的了。

像上面这样,Tabs 和 TabItem 不通过表面的 props 传递也能心有灵犀,二者之间有某种神秘的“组合”,就是我们所说的“组合组件”。

实现方式

上面我们说过,利用 Context API,可以实现组合组件,但是那样 TabItem 需要应用 render props,至于如何实现,读者可以参照上一节的介绍自己尝试。

在这里,我们用一种更巧妙的方式来实现组合组件,可以避免 TabItem 的复杂化。

我们先写出 TabItem 的代码,如下:

jsx
const TabItem = (props) => {
	const { active, onClick } = props;
	const tabStyle = {
		'max-width': '150px',
		color: active ? 'red' : 'green',
		border: active ? '1px red solid' : '0px',
	};
	return (
		<h1 style={tabStyle} onClick={onClick}>
			{props.children}
		</h1>
	);
};

TabItem 有两个重要的 props:active 代表自己是否被激活,onClick 是自己被点击时应该调用的回调函数,这就足够了。

TabItem 所做的就是根据这两个 props 渲染出 props.children,没有任何复杂逻辑,是一个活脱脱的“傻瓜组件”,所以,用一个纯函数实现就可以了。

接下来要做的,就看 Tabs 如何把 active 和 onClick 传递给 TabItem。

我们再来看一下使用组合组件的 JSX 代码:

没有 props 的传递啊,怎么悄无声息地把 active 和 onClick 传递给 TabItem 呢?

Tabs 虽然可以访问到作为 props 的 children,但是到手的 children 已经是创造好的元素,而且是不可改变的,Tabs 是不可能把创造好的元素再强塞给 children 的。

怎么办?

办法还是有的,如果 Tabs 并不去渲染 children,而是把 children 拷贝一份,就有机会去篡改这份拷贝,最后渲染这份拷贝就好了。

我们来看 Tabs 的实现代码:

jsx
class Tabs extends React.Component {
	state = {
		activeIndex: 0
	}

	render() {
		const newChildren = React.Children.map(this.props.children, (child, index) => {
			if (child.type) {
				return React.cloneElement(child, {
					active: this.state.activeIndex === index,
					onClick: () => this.setState({ activeIndex: index })
				});
			} else {
				return child;
			}
		});

		return (
			<Fragment>
				{newChildren}
			</Fragment>
		);
	}
}

在 render 函数中,我们用了 React 中不常用的两个 API:

React.Children.map React.cloneElement

使用 React.Children.map,可以遍历 children 中所有的元素,因为 children 可能是一个数组嘛。

使用 React.cloneElement 可以复制某个元素。这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props,我们就是利用这个机会,把 active 和 onClick 添加了进去。

这两个 API 双剑合璧,就能实现不通过表面的 props 传递,完成两个组件的“组合”。

点击任何一个 TabItem,其样式就会立刻改变。而维护哪个 TabItem 是当前选中的状态,则是 Tabs 的责任。

实际应用

从上面的代码可以看出来,对于组合组件这种实现方式,TabItem 非常简化; Tabs 稍微麻烦了一点,但是好处就是把复杂度都封装起来了,从使用者角度,连 props 都看不见。

所以,应用组合组件的往往是共享组件库,把一些常用的功能封装在组件里,让应用层直接用就行。 在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。

如果你的某两个组件并不需要重用,那么就要谨慎使用组合组件模式,毕竟这让代码复杂了一些。

小结

这一小节介绍了组合组件(Compound Component)这种模式,这是一种比较高级的模式,如果要开发需要关联的成对组件,可以采用这个方案。

Contributors

Long Mo
文章作者:Long Mo
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Longmo Docs