Stateless functional components 以及 PureRender

Stateless functional components 以及 PureRenderMixin 是 React 开发中很重要的两个概念,二者实则存在着千丝万缕的联系,对大多数人来说很难理清它们的关系。本文目的是搞清楚这两个概念,顺便澄清一个很广泛的误解,并分享一些乱七八糟的想法。

Stateless functional components(SFC) 是什么

Stateless functional components 是 React Component 的一种简写方式:

function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}

甚至

const HelloMessage = (props) => <div>Hello {props.name}</div>;

This simplified component API is intended for components that are pure functions of their props

这一写法适用于那些本身是其 props 的纯函数的 Component。

  • stateless:没有 state ,信息都来自 props
  • functional:以函数的形式实现,接受一个参数 props,返回结果与普通 Component 的 render 方法相同
  • component:可以像一个普通的 React Compoennt 那样被使用(jsx)

注意,SFC 在运行时不会真正地对应一个 Component 实例,因此也没有普通 Component 的生命周期方法,不可以通过 ref 绑定(即不可通过 ref 获取到实例)。

为什么推荐使用 Stateless functional components 的形式

  • 语法简洁

  • 无 state 的 component(render 是 props 的纯函数)更易读易维护易测试

    所以能写成 SFC 的尽可能写成 SFC 形式,帮助写出好代码:P

PureRender 是什么

PureRender,即 render 方法是 “pure” 的;

“pure”,即:

it renders the same result given the same props and state

props 及 state 不变,则 render 结果不变。符合该特点的 Component,我们称它为 PureRender Component。

PureRenderMixin 是提供给 PureRender Component,起到优化性能效果的 Mixin。

后面我们提到 PureRender Component,默认它是通过 PureRenderMixin 或类似手段进行了性能优化的,在 props 及 state 不变的情况下不会重新 render。那么具体是怎么优化的呢?

  • shouldComponentUpdate

    Under the hood, the mixin implements shouldComponentUpdate, in which it compares the current props and state with the next ones and returns false if the equalities pass.

    PureRenderMixin 通过复写 Component 的 shouldComponentUpdate 方法实现。若 props 与 state 未发生变化,则返回 false

  • shallow compare

    在 shouldComponentUpdate 中,只是对新旧 props 及 state 做了浅层比较,如果不能保证 immutable,则在深层数据发生变化时出现数据更新但视图未更新的问题。

Stateless functional components 有类似 PureRender Component 的优化吗

即,SFC 有没有默认的类似 PureRenderMixin / shouldComponentUpdate 的机制?答:没有。

即,即使 props 没有变化(SFC 没有 state),函数本身也会被执行(render)一遍。“SFC 天生是 Pure 的”,这很容易被误解,我们会说 SFC 是它的 props 的 pure function,事实上可以认为 SFC 都是符合 PureRender Component 的,然而它们并没有默认享受到类似 PureRenderMixin 那样的优化,证据在此。甚至,因为 SFC 没有生命周期方法,因为是无法像普通的 PureRender Component 那样通过 shouldComponentUpdate 进行优化的。

有几点要注意的:

  • SFC 的性能优化是计划中的,只是还没做
  • PureRenderMixin 这样的优化一般对含 state 的 Component 作用尤其明显
  • 然而这一做法有被滥用的风险,有时候未必能带来性能提升:

关于最后一点,我们做一个简单的比较:

普通 Component 的行为:

  • props 变化了 -> render
  • props 没变化 -> render

PureRender Component 的行为:

  • props 变化了 -> props 遍历及比较 + render
  • props 没变化 -> props 遍历及比较

在 props 变化了的情况下,做了 PureRender Component 会有额外的开销;其收益来自于 props 不变的情况。

什么时候应该使用 Stateless functional components,什么时候使用 ES6 class 的形式

本来的共识:如果 component 是 stateless 的,尽可能用 SFC 的形式。

然而尴尬之处有:

  1. SFC 是慢的,在性能上很多情况下不如 PureRender Component,注:虽然 ES6 的写法不支持 Mixin,但仍然可以通过在 constructor 中复写 shouldComponentUpdate 达到一样的效果。然而对于 SFC 来说,如上所述,目前是做不到的。

  2. 需要向子元素添加 callback,而 callback 行为受 props 影响的时候

class Example extends Component {
  constructor () {
    super();
    this.handleClick = () => this.props.action(this.props.id);
  }
  render() {
    return <button onClick={this.handleClick} />;
  }
}

这里如果写成 SFC 的形式,就是:

function Example (props) {
  return <button onClick={() => props.action(props.id)} />;
}

注意这么做是有性能问题的 —— 每次 Example 的执行(事实上每次都会执行,因为没有类似 PureRender 那样的优化)都会产生一个新的函数 () => props.action(props.id),意味着接受这个函数作为 prop 的子 Component 的重新 render,即使这里子控件不是 button 而是一个 PureRender Component,也会因为 props.onClick 的改变不得不重新 render。这也正是为什么 class 的写法中把 handleClick 这个方法的定义放在 constructor 而不是 render 中的原因(常见的在 constructor 中将很多实例方法进行 bind 的行为同理,在 render 中 bind 或使用匿名/箭头函数会导致产生新的函数)。

所以在这种情况下,至少目前,写成 ES6 class 形式要比 SFC 会更合理。

redux 以及 redux-react 扮演的角色

redux 以及 redux-react 正在越来越多地推动 Compoenent 内部 state 的抽出 —— 将状态从局部 state 转移到单一的顶层 store 中。这些受影响的 state 就变成了 props,即,我们的 Component 在越来越多地变成 stateless 的,即,我们写出越来越多的 SFC。

前面提到 SFC 是无法通过 PureRenderMixin 或者像 ES6 class Component 那样复写 shouldComponentUpdate 实现性能优化的,那么通过 HOC(higher-order component)呢?答案是肯定的,redux-react 提供的 connect 方法就是一个例子,然而不同于一般的 HOC 在 render 方法中直接返回 <ChildComponent {...this.props} /> 的方式,connect 在内部实现中比较 HACK 地手动生成并缓存了子 Component 的渲染结果。

所以实现一个 PureRender HOC for SFC 是可行的,除了像 connect 那样的做法外,还可以利用容器 Component 本身的 shouldComponentUpdate,只是这样会将对应的 SFC 又包裹了一层普通的 class 形式的 Component,在一定程度上失去了写成 SFC 的意义。具体优化效果也是需要在实际使用才能评判的。

TODO

找一个或者实现一个纯粹的 PureRender HOC for SFC,使用并衡量效果。

参考

  • https://github.com/facebook/react/issues/5677
  • https://github.com/yannickcr/eslint-plugin-react/issues/491
  • https://github.com/facebook/react/issues/5677#issuecomment-172467526
  • https://github.com/reactjs/redux/issues/1176