背景

React 在渲染出的 UI 内部建立和维护了一个内层的实现方式,它包括了从组件返回的 React 元素。这种实现方式使得 React 避免了一些不必要的创建和关联 DOM 节点,因为这样做可能比直接操作 JavaScript 对象更慢一些,它被称之为“虚拟DOM”

当一个组件的 props 或者 state 改变时,React通过比较新返回的元素和之前渲染的元素来决定是否有必要更新实际的 DOM。当他们不相等时,React才会更新DOM

在一些情况下,你的组件可以通过重写这个生命周期函数 shouldComponentUpdate 来提升速度, 它是在重新渲染过程开始前触发的。这个函数默认返回true,可以使 React 执行更新:

  1. shouldComponentUpdate(nextProps, nextState) {
  2. return true;
  3. }

这意味着就算没有改变组件的 props 或者 state,也会导致组件的重绘。这就经常导致组件因为不相关数据的改变导致重绘,这极大的降低了 React 的渲染效率。比如下面的例子中,任何 options 的变化,甚至是其他数据的变化都可能导致所有 cell 的重绘

  1. //Table Component
  2. {this.props.items.map(i =>
  3. <Cell data={i} option={this.props.options[i]} />
  4. )}

为了避免这个问题,我们可以在Cell中重写shouldComponentUpdate方法,只在option发生改变时进行重绘

  1. class Cell extends React.Component {
  2. shouldComponentUpdate(nextProps, nextState) {
  3. if (this.props.option === nextProps.option) {
  4. return false;
  5. } else {
  6. return true;
  7. }
  8. }
  9. }

当你的组件变得更加复杂时,你可以使用类似的模式来做一个“浅比较”,用来比较属性和值以判定是否需要更新组件。这种模式十分常见,因此React提供了一个辅助对象来实现这个逻辑 —- 继承自 React.PureComponent

原理

当组件更新时,如果组件的 props 和 state 都没发生改变, render 方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。具体就是 React 自动帮我们做了一层浅比较

  1. if (this._compositeType === CompositeTypes.PureClass) {
  2. shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextStat e);
  3. }
  4. return shouldUpdate;

shallowEqual 从字面意思讲是”浅比较”,那么什么是浅比较呢?

就是比较一个旧的 props 和新 props 的或者旧的 state 和新的 state 的长度是否一致,key 值是否相同,以及它们对应的引用是否发生改变,仅仅做了一层比较,所以这才叫做浅比较

shallowEqual 的源码为:

  1. /**
  2. * Performs equality by iterating through keys on an object and returning false
  3. * when any key has values which are not strictly equal between the arguments.
  4. * Returns true when the values of all keys are strictly equal.
  5. */
  6. function shallowEqual(objA: mixed, objB: mixed): boolean {
  7. if (is(objA, objB)) {
  8. return true;
  9. }
  10. if (
  11. typeof objA !== 'object' ||
  12. objA === null ||
  13. typeof objB !== 'object' ||
  14. objB === null
  15. ) {
  16. return false;
  17. }
  18. const keysA = Object.keys(objA);
  19. const keysB = Object.keys(objB);
  20. if (keysA.length !== keysB.length) {
  21. return false;
  22. }
  23. // Test for A's keys different from B.
  24. for (let i = 0; i < keysA.length; i++) {
  25. if (
  26. !hasOwnProperty.call(objB, keysA[i]) ||
  27. !is(objA[keysA[i]], objB[keysA[i]])
  28. ) {
  29. return false;
  30. }
  31. }
  32. return true;
  33. }

shallowEqual 函数完成的功能:

  • 通过 is 函数对两个参数进行比较,判断是否相同,相同直接返回true(基本数据类型值相同、同一个引用对象都表示相同)
  • 如果两个参数不相同,判断两个参数是否至少有一个不是引用类型,存在即返回false,如果两个都是引用类型对象,则继续下面的比较
  • 判断两个不同引用类型对象是否相同:先通过 Object.keys 获取到两个对象的所有属性,具有相同属性,且每个属性值相同即两个对象相同(相同也通过 is 函数完成)

其中 is 函数是自己实现的一个 Object.is 的功能,排除了===两种不符合预期的情况:

  1. +0 === -0 // true
  2. NaN === NaN // false
  1. /**
  2. * inlined Object.is polyfill to avoid requiring consumers ship their own
  3. * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
  4. */
  5. function is(x, y) {
  6. // SameValue algorithm
  7. if (x === y) {
  8. // Steps 1-5, 7-10
  9. // Steps 6.b-6.e: +0 != -0
  10. // Added the nonzero y check to make Flow happy, but it is redundant,排除 +0 === -0的情况
  11. return x !== 0 || y !== 0 || 1 / x === 1 / y;
  12. } else {
  13. // Step 6.a: NaN == NaN,x和y都不是NaN
  14. return x !== x && y !== y;
  15. }
  16. }

问题

大部分情况下,你可以使用 React.PureComponent 而不必写你自己的 shouldComponentUpdate,它只做一个浅比较,但是由于浅比较会忽略属性或状态突变的情况,此时你不能使用它

  1. // ListOfWords
  2. class ListOfWords extends React.PureComponent {
  3. render() {
  4. return <div>{this.props.words.join(',')}</div>;
  5. }
  6. }
  7. // WordAdder
  8. class WordAdder extends React.Component {
  9. constructor(props) {
  10. super(props);
  11. this.state = {
  12. words: ['marklar']
  13. };
  14. this.handleClick = this.handleClick.bind(this);
  15. }
  16. handleClick() {
  17. // This section is bad style and causes a bug
  18. const words = this.state.words;
  19. words.push('marklar');
  20. this.setState({words: words});
  21. }
  22. render() {
  23. return (
  24. <div>
  25. <button onClick={this.handleClick} />
  26. <ListOfWords words={this.state.words} />
  27. </div>
  28. );
  29. }
  30. }

在 ListOfWords 中,this.props.words 是 WordAdder 中传入的其 state 的一个引用,虽然在 WordAdder 的handelClick 方法中被改变了,但是对于 ListOfWords 来说,其引用是不变的,从而导致并没有被更新

解决方法

在上面的问题中可以发现,当一个数据是不变数据时,可以使用一个引用。但是对于一个易变数据来说,不能使用引用的方式给到 PureComponent。简单来说,就是我们在 PureComponent 外层来修改其使用的数据时,应该给其赋值一个新的对象或者引用,从而才能确保其能够进行重新渲染。例如上面例子中的 handleClick 可以通过以下几种方式来进行修改从而进行正确的渲染:

  1. handleClick() {
  2. this.setState(prevState => ({
  3. words: prevState.words.concat(['marklar'])
  4. }));
  5. }

或者:

  1. handleClick() {
  2. this.setState(prevState => ({
  3. words: [...prevState.words, 'marklar'],
  4. }));
  5. };

或者针对对象结构:

  1. function updateColorMap(oldObj) {
  2. return Object.assign({}, oldObj, {key: new value});
  3. }

Immutable.js 是解决这个问题的另一种方法,它通过结构共享提供不可突变的,持久的集合:

  • 不可突变:一旦创建,集合就不能在另一个时间点改变
  • 持久性:可以使用原始集合和一个突变来创建新的集合,原始集合在新集合创建后仍然可用
  • 结构共享:新集合尽可能多的使用原始集合的结构来创建,以便将复制操作降至最少从而提升性能
  1. // 常见的js处理
  2. const x = { foo: 'bar' };
  3. const y = x;
  4. y.foo = 'baz';
  5. x === y; // true
  6. // 使用 immutable.js
  7. const SomeRecord = Immutable.Record({ foo: null });
  8. const x = new SomeRecord({ foo: 'bar' });
  9. const y = x.set('foo', 'baz');
  10. x === y; // false

总结

从上面我们发现 PureComponent 并不是不发生重新渲染,而是在满足不了的条件下才会去帮我们重新渲染,所以PureComponent 真正起到优化的地方是在纯组件上,也就是仅仅用来展示的组件,这样会避免展示的组件不必要的多次重复渲染。在一些复杂组件上用了也没有什么关系,只是在浅比较这一层就过不了,但是一定要记得 props 和state 不可以是同一个引用