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 !