上一篇介绍了三种设计模式,包括1.容器与展示组件 2.高阶组件 3.render props。
这篇我们继续介绍三种设计模式,包括1.context模式 2.高阶组件 3.继承模式
Context模式
概念介绍
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
示例
React v16.3.0前后的Context相关API不同,这边只介绍新版本的Context使用方法。
第一步:新建createContext
首先,要用新提供的 createContext 函数创造一个“上下文”对象。
1
const ThemeContext = React.createContext();
第二步:生成Provider 和 Consumer
接着,我们用ThemeContext生成两个属性,分别是Provider和Consumer。从字面意思即可理解。Provider供数据提供者使用,Consumer供数据消费者使用。1
2const ThemeProvider = ThemeContext.Provider;
const ThemeConsumer = ThemeContext.Consumer;
第三步:使用ThemeProvider给数据提供者
1 | const Context = () => { |
第四步:使用ThemeConsumer给数据接收者
1 | // 这里演示一个class组件。Counsumer使用了renderProps模式哦。 |
模式所解决的问题
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。
react-router就使用了这种模式。
注意事项
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:1
2
3
4
5
6
7
8
9class App extends React.Component {
render() {
return (
<Provider value={{something: 'something'}}>
<Toolbar />
</Provider>
);
}
}
为了防止这种情况,将 value 状态提升到父节点的 state 里:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}
Compound Component
概念介绍
通过组合组件,使用者只需要传递子组件,子组件所需要的props在父组件会封装好,引用子组件的时候就没必要传递所有props了。
组合组件核心的两个方法是React.Children.map和React.cloneElement。React.Children.map用来遍历获得组件的子元素,React.cloneElement则用来复制元素,这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props,我们就是利用这个函数,把 想要的props传入子元素。
示例
我们设计一个类似于antd中的Tabs组件,提供tab切换功能,而被选中的TabItem需要高亮。
如果我们使用常规写法,用 Tabs 中一个 state 记录当前被选中的 Tabitem 序号,然后根据这个 state 传递 props 给 TabItem,还需要传递一个 onClick 事件进去,捕获点击选择事件。1
2
3<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和onClick,我们用compound模式解决这个问题。
1 | const TabItem = (props) => { |
重点在于Tabs是如何实现的,我们来看下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class 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>
);
}
}
通过组合使用React.Children.map和React.cloneElement,我们让TabItem获得了它想要的属性,是不是很简单!
模式所解决的问题
组合组件设计模式一般应用在一些共享组件上。如select和option, Tab和TabItem等,通过组合组件,使用者只需要传递子组件,子组件所需要的props在父组件会封装好,引用子组件的时候就没必要传递所有props了。
我们会发现大量的共享库当中运用了这种模式,比如antd。
继承模式
说了那么多的模式,我们别忘了可能我们很熟悉的继承模式。如果组件定义为class组件,那么我们当然可以使用继承的模式来实现组件的复用。
示例
我们通过一个基类来实现一些通用的逻辑,然后再通过继承分别实现两个子类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Base extends React.PureComponent {
getAlbumItem = () => {
return null
}
render () {
return (
<div style={{border:'1px solid red',margin:5,width:300}}>
{this.getAlbumItem()}
<div>通用逻辑写这里</div>
</div>
)
}
}
class Mobile extends Base {
getAlbumItem = () => {
return <span>mobile</span>
}
}
class Pc extends Base {
getAlbumItem = () => {
return <span>pc</span>
}
}
我们可以看到Mobile和Pc共享了Base的逻辑,实现了复用。
组合与继承
继承看起来很方便,也很好理解。但是react官方并不推荐使用继承,因为组合的模式已经够用了。
继承有两个缺点,其一是,父类的属性和方法,子类是无条件继承的。也就是说,不管子类愿意不愿意,都必须继承父类所有的属性和方法。其二是,js中class并不直接支持多继承。这两个缺点使得继承相对于组合组件缺少了灵活性以及可扩展性。请记住,组合优于继承!
尾声
其实前面说知识点都能在react官方文档查到,但官方文档组织地有点凌乱。读者可在读完这篇文章后具体去查找相应章节作为巩固。
参考链接: