1. 前言
在 React 中,一切皆是组件,因此理解组件的工作流与核心尤为重要。
我们有多种创建组件的方式(不仅 Component),很多时候选择使用哪种组件的创建方式是值得深入考究的;同时对于 React 中有太多的组件概念,无状态组件、高阶组件… 常常也是让新手一头雾水,因此本文也尝试解释分析不同的组件概念。
2. 组件的创建方式
2.1 Component
这是 React 中最常见与最通用的组件创建方式:
class Container extends React.Component {
construcor (props) {
super(props);
this.state = {};
}
render () {
return (
<div className="container">{ this.props.children }</div>
);
}
}
使用了 es6 中类的继承方法,当然它也有 es5 的写法(createClass):
var Container = React.createClass({
getInitialState: function() {
return {};
},
render () {
return (
<div className="container">{ this.props.children }</div>
);
}
});
两种方法都是一样的返回一个 Container
的组件类,这是我们通常创建组件的方式,因此不做更多阐述了。
2.2 PureComponent
首先我们来理解下 React 组件执行重渲染(re-render)更新的时机,一般当一个组件的 props (属性)或者 state (状态)发生改变的时候,也就是父组件传递进来的 props 发生变化或者使用 this.setState
函数时,组件会进行重新渲染(re-render);
而在接受到新的 props 或者 state 到组件更新之间会执行其生命周期中的一个函数 shouldComponentUpdate
,当该函数返回 true
时才会进行重渲染,如果返回false
则不会进行重渲染,在这里 shouldComponentUpdate
默认返回 true
;
因此当组件遇到性能瓶颈的时候可以在 shouldComponentUpdate
中进行逻辑判断,来自定义组件是否需要重渲染。
PureComponent 是在 react v15.3.0 中新加的一个组件,从 React 源码中可以看到它是继承了 Component 组件:
/**
* Base class helpers for the updating state of a component.
*/
function ReactPureComponent(props, context, updater) {
// Duplicated from ReactComponent.
this.props = props;
this.context = context;
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;
var pureComponentPrototype = (ReactPureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = ReactPureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, ReactComponent.prototype);
pureComponentPrototype.isPureReactComponent = true;
同时在shouldComponentUpdate
函数中有一段这样的逻辑:
if (type.prototype && type.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
因此 PureReactComponent 组件和 ReactComponent 组件的区别就是它在 shouldComponentUpdate 中会默认判断新旧属性和状态是否相等,如果没有改变则返回 false
,因此它得以减少组件的重渲染。
当然,这里对新旧属性和状态的比较都为类的浅比较。
优点:
- 在 shouldComponentUpdate 生命周期做了优化会自动 shadow diff 组件的 state 和 props,结合 immutable 数据就可以很好地去做更新判断;
- 隔离了父组件与子组件的状态变化;
缺点:
- shouldComponentUpdate 中的 shadow diff 同样消耗性能;
- 需要确保组件渲染仅取决于 props 与 state ;
2.3 函数式组件
在 React 中还可以以函数来定义一个组件,称之为函数式组件:
const Button = ({ children, ...props }) => (
<button {...props}>{children}</button>
);
函数式组件又称为无状态(stateless)组件,它不存在自身的状态,并且没有普通组件中的各种生命周期方法,同时其函数式的写法决定了其渲染只由属性决定;
优点:
- 简化代码、专注于 render;
- 组件不需要被实例化,无生命周期,提升性能;
- 输出(渲染)只取决于输入(属性),无副作用;
- 视图和数据的解耦分离;
缺点:
- 无法使用 ref;
- 无生命周期方法;
- 无法控制组件的重渲染,因为无法使用 shouldComponentUpdate 方法,当组件接受到新的属性时则会重渲染;
3. Component 与 PureComponent 的选择
先看一个例子:
class UserAvatar extends React.Component {
render() {
console.log('UserAvatar re-render');
return (
<div>
< img src={this.props.imageUrl} />
</div>
);
}
}
class Container extends React.Component {
construcor (props) {
super(props);
this.state = {
name: '',
avatar: "//xxx.xxx/xxxx",
};
}
render () {
const { name, avatar } = this.state;
console.log('Container re-render');
return (
<div className="container">
<UserAvatar imageUrl={} />
<div>{name}</div>
<button onClick={() => this.setState({ name: 'n' })}>CLICK</button>
</div>
);
}
}
例子中 Container 组件为 UserAvatar 组件的父组件,当 Container 组件的 state 中的 name 发生改变的时候,Container 执行重渲染,而 UserAvatar 也执行了重渲染,即使它的属性没有发生改变;
虽然在 React 中实现的是 diff 比较,实际的 dom 并不一定会被更新,但是 diff 的比较也是非常消耗性能的;
当把 UserAvatar 改成继承 PureComponent 之后,那么它将会在 shouldComponentUpdate 进行浅比较,如果属性没有改变,则不会进行重渲染。
因此相比于 Component ,PureComponent 有性能上的更大提升:
- 减少了组件无意义的重渲染(当 state 和 props 没有发生变化时),当结合 immutable 数据时其优更为明显;
- 隔离了父组件与子组件的状态变化;
当我们开始使用 PureComponent 组件,并不需要做更多的事情,所做的仅仅是将 Component 替换成 PureComponent;
那么既然 PureComponent 相比 Component 对性能和渲染上做了更多的优化处理,那么我们是否应该在所有地方都使用 PureComponent 替换 Component 吗?
答案当然是否定的,如果是这样那么 React 官方早应该使用 PureComponent 作为其默认组件了。
原因如下:
- 我们应该避免过早优化,当在应用出现性能瓶颈的时候才需要去排查与解决这部分的渲染;
- 在 shouldComponentUpdate 中先置进行新旧属性与状态的浅比较同样是对于性能上的消耗,而其带来的优化效果与性能消耗还需结合实际情况进行抉择;
- PureComponent 在 shouldComponentUpdate 所做的也仅是浅层的对象比较,在属性/状态层级结构较深较复杂的情况下容易出现深层 bug,当然如果引入了 immutable 数据那么这里的风险将会大大减小;
- 在我们真正遇到性能瓶颈时,很多时候的处理并不仅仅是比较属性/状态是否改变,因此 PureComponent 在这种情况下优势也不大。
4. 选择函数式组件
那么何时使用函数式组件呢?
- 对于函数式组件,由于它不需要处理复杂的生命周期函数,因此它在性能上也有一定优势。
- 当一个展示性组件的渲染仅仅依赖于其属性,使用函数式组件可以保证它的“纯”,并且对组件本身无任何副作用;
- 由于它无法控制它的重渲染(无 shouldComponentUpdate 生命周期),因此我们希望该组件的属性数据相对较少,同时组件本身结构相对简单;
- 函数式组件能够更好地与业务抽离,并且易于测试。
因此对于简单的通用性组件,比如自定义的 <Button />、<Input /> 等组件选择函数式组件是再好不过的了。
5. 纯组件与无状态组件
那么什么样的组件才算是 “纯” 的呢?
React 中将 PureComponent 定义为纯组件,假如一个组件只和 props 和 state 有关系,给定相同的 props 和 state 就会渲染出相同的结果,且不受副作用影响,那么这个组件就叫做纯组件,或者说纯组件只依赖于组件的 props 和 state 。
那么使用 PureComponent 的组件就一定是所谓的 “纯” 组件了吗?
事实上,在 PureComponent 中仍然会在生命周期中对其产生副作用,比如你可以在 componentDidMount 发送 ajax 请求,或者通过计算 dom 去改变某个 div 的高度。
因此组件的 “纯” 取决于你实际代码中是如何实现的。
�
而对于无状态组件它仅由其属性决定,且它通过函数式定义因此不存在副作用,无状态组件也是一种 “纯” 组件。
6. Smart 组件与 Dumb 组件
当应用的视图层与数据层解耦的情况下,比如结合 Redux 或者 Mobx 等状态管理库,那么在一个应用中组件又分为 Smart 组件与 Dumb 组件。
6.1 Smart 组件
Smart 组件又称为 容器组件,它负责处理应用的数据以及业务逻辑,�同时将状态数据与操作函数作为属性传递给子组件;
一般而言它仅维护很少的 DOM,其所有的 DOM 也仅是作为布局等作用。
6.2 Dumb 组件
Dumb 组件又称为 木偶组件,它负责展示作用以及响应用户交互,它一般是无状态的(在如 Modal 等类组件中可能会维护少量自身状态);
一般而言 Dumb 组件会拆分为一个个可复用、功能单一的组件;
因此 Dumb 组件使用函数式组件定义,当其需要对重渲染进行优化时则可以使用 PureComponent。
所以 Smart 组件更多关注与数据以及业务逻辑,而 Dumb 组件与数据和业务解耦,主要复杂 UI 层面的展示与交互。
7. 高阶组件
高阶组件(higher-order-components)是react中对组件逻辑进行重用的高级技术。但高阶组件本身并不是React API。它只是一种模式,这种模式是由react自身的组合性质必然产生的。
这里需要明确一点:高阶组件并不是一个组件类,它是一个函数,接收一个组件并返回一个新组件。
const newComponent = higherOrderComponent(oldComponent);
高阶组件(HOC)是一种修饰者模式,它对原有组件进行改造并生成新的组件,对组件代码进行的复用。
�
下面例子�来自 https://discountry.github.io/react/docs/higher-order-components.html;
假设有一个 CommentList 组件:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
之后你又有一个 BlogPost 组件:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
CommentList 和 BlogPost 组件并不相同 —— 他们调用了 DataSource 的不同方法获取数据,并且他们渲染的输出结果也不相同。但是,他们的大部分实现逻辑是一样的:
- 挂载组件时候监听数据变化;
- 数据变化是改变组件状态;
- 组件卸载时移除监听函数;
设想一下,在一个大型的应用中,这种从 DataSource 订阅数据并调用 setState 的模式将会一次又一次的发生。我们就可以抽象出一个模式,该模式允许我们在一个地方定义逻辑并且能对所有的组件使用,这就是高阶组件的精华所在。
我们写一个函数,该函数能够创建类似 CommonList 和 BlogPost 从 DataSource 数据源订阅数据的组件 。该函数接受一个子组件作为其中的一个参数,并从数据源订阅数据作为props属性传入子组件。我们把这个函数取个名字 withSubscription:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
});
第一个参数是包裹组件(wrapped component),第二个参数会从 DataSource和当前props 属性中检索应用需要的数据。
当 CommentListWithSubscription 和 BlogPostWithSubscription 渲染时, 会向CommentList 和 BlogPost 传递一个 data props属性,该 data属性的数据包含了从 DataSource 检索的最新数据:
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
因为 withSubscription 就是一个普通函数,你可以添加任意数量的参数。例如,你或许会想使 data 属性可配置化,使高阶组件和包裹组件进一步隔离开。或者你想要接收一个参数用于配置 shouldComponentUpdate 函数,或配置数据源的参数。这些都可以实现,因为高阶组件可以完全控制新组件的定义。
在很多第三方库中都使用到了高阶组件,比如 react-router 中的 connect 就是连接了 redux 状态与组件的高阶组件,或者 react-router 中的 withRouter 用来给组件注入 �history 数据。
参考
- https://news.ycombinator.com/item?id=14418576
- https://discountry.github.io/react/docs/higher-order-components.html
- https://stackoverflow.com/questions/40703675/react-functional-stateless-component-purecomponent-component-what-are-the-dif
来自阿诚的日志