React 中的各种组件

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,因此它得以减少组件的重渲染。
当然,这里对新旧属性和状态的比较都为类的浅比较。

优点:

  1. 在 shouldComponentUpdate 生命周期做了优化会自动 shadow diff 组件的 state 和 props,结合 immutable 数据就可以很好地去做更新判断;
  2. 隔离了父组件与子组件的状态变化;

缺点:

  1. shouldComponentUpdate 中的 shadow diff 同样消耗性能;
  2. 需要确保组件渲染仅取决于 props 与 state ;

2.3 函数式组件

在 React 中还可以以函数来定义一个组件,称之为函数式组件:

const Button = ({ children, ...props }) => (
  <button {...props}>{children}</button>
);

函数式组件又称为无状态(stateless)组件,它不存在自身的状态,并且没有普通组件中的各种生命周期方法,同时其函数式的写法决定了其渲染只由属性决定;

优点:

  1. 简化代码、专注于 render;
  2. 组件不需要被实例化,无生命周期,提升性能;
  3. 输出(渲染)只取决于输入(属性),无副作用;
  4. 视图和数据的解耦分离;

缺点:

  1. 无法使用 ref;
  2. 无生命周期方法;
  3. 无法控制组件的重渲染,因为无法使用 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 有性能上的更大提升:

  1. 减少了组件无意义的重渲染(当 state 和 props 没有发生变化时),当结合 immutable 数据时其优更为明显;
  2. 隔离了父组件与子组件的状态变化;

当我们开始使用 PureComponent 组件,并不需要做更多的事情,所做的仅仅是将 Component 替换成 PureComponent;

那么既然 PureComponent 相比 Component 对性能和渲染上做了更多的优化处理,那么我们是否应该在所有地方都使用 PureComponent 替换 Component 吗?
答案当然是否定的,如果是这样那么 React 官方早应该使用 PureComponent 作为其默认组件了。
原因如下:

  1. 我们应该避免过早优化,当在应用出现性能瓶颈的时候才需要去排查与解决这部分的渲染;
  2. 在 shouldComponentUpdate 中先置进行新旧属性与状态的浅比较同样是对于性能上的消耗,而其带来的优化效果与性能消耗还需结合实际情况进行抉择;
  3. PureComponent 在 shouldComponentUpdate 所做的也仅是浅层的对象比较,在属性/状态层级结构较深较复杂的情况下容易出现深层 bug,当然如果引入了 immutable 数据那么这里的风险将会大大减小;
  4. 在我们真正遇到性能瓶颈时,很多时候的处理并不仅仅是比较属性/状态是否改变,因此 PureComponent 在这种情况下优势也不大。

4. 选择函数式组件

那么何时使用函数式组件呢?

  1. 对于函数式组件,由于它不需要处理复杂的生命周期函数,因此它在性能上也有一定优势。
  2. 当一个展示性组件的渲染仅仅依赖于其属性,使用函数式组件可以保证它的“纯”,并且对组件本身无任何副作用;
  3. 由于它无法控制它的重渲染(无 shouldComponentUpdate 生命周期),因此我们希望该组件的属性数据相对较少,同时组件本身结构相对简单;
  4. 函数式组件能够更好地与业务抽离,并且易于测试。
    因此对于简单的通用性组件,比如自定义的 <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 的不同方法获取数据,并且他们渲染的输出结果也不相同。但是,他们的大部分实现逻辑是一样的:

  1. 挂载组件时候监听数据变化;
  2. 数据变化是改变组件状态;
  3. 组件卸载时移除监听函数;

设想一下,在一个大型的应用中,这种从 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 数据。

参考

来自阿诚的日志

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容