组件设计模式(1):聪明组件和傻瓜组件
从这一节开始,我们来介绍React中的组件设计模式。
在 React 应用中,最简单也是最常用的一种组件模式,就是“聪明组件和傻瓜组件”。
其实,这个模式的名称很多,就我所知,除了“聪明组件和傻瓜组件”,还有这些称呼:
- 容器组件和展示组件(Container and Presentational Components);
- 胖组件和瘦组件;
- 有状态组件和无状态组件。
名字只是一个代号,关键还是要看本质,这种模式的本质,就是把一个功能分配到两个组件中,形成父子关系,外层的父组件负责管理数据状态,内层的子组件只负责展示。
在本小册中,都会以“聪明组件”和“傻瓜组件”称呼这种模式
为什么要分割聪明组件和傻瓜组件
软件设计中有一个原则,叫做 “责任分离”(Separation of Responsibility), 简单说就是让一个模块的责任尽量少,如果发现一个模块功能过多,就应该拆分为多个模块,让一个模块都专注于一个功能,这样更利于代码的维护。
还记得我么说过 React 其实就是这样一个公式吗?
UI = f(data)
使用 React 来做界面,无外乎就是获得驱动界面的数据,然后利用这些数据来渲染界面。 当然,你可以在一个组件中就搞定,但是,最好把获取和管理数据这件事和界面渲染这件事分开。
做法就是,把获取和管理数据的逻辑放在父组件,也就是聪明组件;把渲染界面的逻辑放在子组件,也就是傻瓜组件。
这么做的好处,是可以灵活地修改数据状态管理方式,比如,最初你可能用 Redux 来管理数据,然后你想要修改为用Mobx, 如果按照这种模式分割组件,那么,你需要改的只有聪明组件,傻瓜组件可以保持原状。
随机笑话样例
我们利用一个显示“随机笑话”的功能来演示这种模式,所谓“随机笑话”,就是需要从服务器获取随机的一个笑话,展示在页面上。
功能可以分为两部分,第一部分是展示,也就是傻瓜组件SmileFace,代码如下:
import SmileFace from './yaoming_simile.png';
const Joke = ({ value }) => {
return (
<div>
<img src={SmileFace} />
{value || 'loading...'}
</div>
);
}
傻瓜组件 Joke 的功能很简单,显示一个笑脸,然后显示名为 value 的 props,也就是笑话的内容,如果没有 value 值,就显示一个“loading...”。
至于怎么获得笑话内容,不是 Joke 要操心的事,它只专注于显示笑话,所谓傻人有傻福,傻瓜组件虽然“傻”了一点,但是免去了数据管理的烦恼。
然后是聪明组件,这个组件不用管渲染的逻辑,只负责拿到数据,然后把数据传递给傻瓜组件,由傻瓜组件来完成渲染。
我们把聪明组件命名为 RandomJoke,代码如下:
export default class RandomJoke extends React.Component {
state = {
joke: null
}
render() {
return <Joke value={this.state.joke} />
}
componentDidMount() {
fetch('https://icanhazdadjoke.com/',
{ headers: { 'Accept': 'application/json' } }
).then(response => {
return response.json();
}).then(json => {
this.setState({ joke: json.joke });
});
}
}
可以看到,RandomJoke 的 render 函数只做一件事,就是渲染 Joke,并把 this.state 中的值作为 props 传进去。
聪明组件的 render 函数一般都这样简单,因为渲染不是他们操心的业务,他们的主业是获取数据。
RandomJoke 获取数据的方法是在 componentDidMount 函数中调用一个 API (<icanhazdadjoke.com/>),这个 API 随即返回一个英文笑话。
实话说,这些英文笑话都很冷,也不大容易看懂,但是足够展示通过 API 获取数据的过程。
当 RandomJoke 被第一次渲染的时候,它的 state 中的 joke 值为 null,所以它传给 Joke 的 value 也是 null,
这时候,Joke 会渲染一 “loading...”。
但是,在第一次渲染完毕的时候,componentDidMount 被调用,一个 API 请求发出去,拿到一个随机笑话,更新 state 中的 joke 值。
因为对一个组件 state 的更新会引发一个新的渲染过程,所以 RandomJoke 的 render 再一次被调用, 所以 Joke 也会再一次被渲染,这一次,传入的 value 值是一个真正的笑话,所以,笑话也就出现了。 当然,这个界面非常简陋,获取数据的方法也十分粗糙,但是,最妙的是,应用了这种方法之后,如果你要优化界面,只需要去修改傻瓜组件 Joke,如果你想改进数据管理和获取,只需要去修改聪明组件 RandomJoke。
如此一来,维护工作就简单多了,你甚至可以把两个组件分配各两个不同的开发者去维护开发。
如果应用 Redux 和 Mobx,也会应用到这种模式,我们在后面的小节中会详细介绍更多这种模式的应用。
PureComponent
因为傻瓜组件一般没有自己的状态,所以,可以像上面的 Joke 一样实现为函数形式,其实,我们可以进一步改进,利用 PureComponent 来提高傻瓜组件的性能。
函数形式的 React 组件,好处是不需要管理 state,占用资源少,但是,函数形式的组件无法利用 shouldComponentUpdate。
看上面的例子,当 RandomJoke 要渲染 Joke 时,即使传入的 props 是一模一样的,Joke 也要走一遍完整的渲染过程,这就显得浪费了。
好一点的方法,是 把 Joke 实现为一个类,而且定义 shouldComponentUpdate 函数, 每次渲染过程中,在 render 函数执行之前 shouldComponentUpdate 会被调用, 如果返回 true,那就继续,如果返回 false,那么渲染过程立刻停止,因为这代表不需要重画了。
class Joke extends React.PureComponent {
render() {
return (
<div>
<img src={SmileFace} />
{this.props.value || 'loading...'}
</div>
);
}
}
值得一提的是,PureComponent 中 shouldComponentUpdate 对 props 做得只是浅层比较,不是深层比较,**如果 props 是一个深层对象,就容易产生问题 **。
比如,两次渲染传入的某个 props 都是同一个对象,但是对象中某个属性的值不同, 这在 PureComponent 眼里,props 没有变化,不会重新渲染,但是这明显不是我们想要的结果。
React.memo
虽然 PureComponent 可以提高组件渲染性能,但是它也不是没有代价的,它逼迫我们必须把组件实现为 class,不能用纯函数来实现组件。
如果你使用 React v16.6.0 之后的版本,可以使用一个新功能 React.memo 来完美实现 React 组件,上面的 Joke 组件可以这么写
const Joke = React.memo(({ value }) => (
<div>
<img src={SmileFace} />
{value || 'loading...'}
</div>
));
React.memo 既利用了 shouldComponentUpdate,又不要求我们写一个 class,这也体现出 React 逐步向完全函数式编程前进。
React.memo()可以满足创建纯函数而不是一个类的需求。 React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新,与shouldComponentUpdate()功能类似。
小结
在这一小节中,我们介绍了“聪明组件和傻瓜组件”这种做法简单的 React 设计模式, 读者应该能够理解为什么有时候要把组件分为两个部分,在众多框架中,都可以应用“聪明组件和傻瓜组件”模式。