React性能优化 之 不可变数据

什么是不可变数据

不可变数据的概念来自函数式编程。

在函数式编程中,对已初始化的“变量”是不可以更改的,每次更改都要创建一个新的“变量”。

Javascript 在语言层没有实现不可变数据,需要借助第三方库来实现。(immutable.js 或者 immer.js)


为什么使用不可变数据?

使用不可变数据的目的是为了跟踪数据的改变。

如果直接修改数据,那么就很难跟踪到数据的改变,跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。

但跟踪不可变数据的变化相对来说就容易多了。

如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。

当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。

当它们不相同时,React 会更新该 DOM。

虽然 React 已经保证未变更的元素不会进行更新,但即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。

在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。

如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。


React性能优化离不开不可变值

shouldComponentUdpate中可以接收两个参数,nextProps和nextState,假如我们通过判断this.props.xxx和nextProps.xxx相等以及this.state.xxx与nextState.xxx相等,可以将返回值设置为false,说明此次并不需要更新子组件:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color !== nextProps.color) {
      return true;
    }
    if (this.state.count !== nextState.count) {
      return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={() => this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>
    );
  }
}

React v15.3新增加了一个PureComponent类,能够对props和state进行浅比较来减少render函数的执行次数,避免不必要的组件渲染,实现性能上的优化。

什么是浅比较?

答:我们知道JS中的变量类型分为基本类型(number、string、boolean、undefined、null、symbol)和引用类型(function、object、function),基本类型的值保存在栈内存当中,引用类型的值保存在堆内存当中,栈内存中只保存指向堆内存的引用。

而浅比较就是只对栈内存中的数据进行比较


下面例子使用了PureComponent,而且只改变了数组items里的值,而没改变items的引用地址,所以认为items没有发生变化,不会触发render函数,不会触发组件的渲染。

class App extends PureComponent {
  state = {
    items: [1, 2, 3]
  }
  handleClick = () => {
    const { items } = this.state;
    items.pop();
    this.setState({ items });
  }
  render() {
    return (
        <div>
            <ul>
                {this.state.items.map(i => <li key={i}>{i}</li>)}
            </ul>
            <button onClick={this.handleClick}>delete</button>
        </div>
    )
  }
}

如果想实现组件更新,可以按如下的方式,创建一个新的数组,将新数组的地址赋给items,这样子栈内存中的引用就改变了:

handleClick = () => {
    const { items } = this.state;
    items.pop();
    var newItem = [...items];
    this.setState({ item: newItem });
}

immutable.js / immer.js

PureComponent帮我们做了浅层的比较。

如果需要出现引用嵌套引用的数据结构的话,类似下述结构:

handleClick() {
  this.setState(state => ({
    objA: {
      ...state.objA,
      objB: {
        ...state.objA.objB,
        objC: {
          ...state.objA.objB.objC,
          stringA: 'string',
        }
      },
    },
  }));
};

这样子每一层都使用展开运算符,来改变栈内存中的引用地址,太过繁琐。


那为什么不使用深拷贝呢?

答:深拷贝会让所有组件都接收到新的数据,让 shouldComponentUpdate 失效。深比较每次都比较所有值,当数据层次很深且只有一个值变化时,这些比较是对性能的浪费。

视图层的代码,我们希望它更快响应,所以使用 immutable 库进行不可变数据的操作,也算是一种空间换时间的取舍。


immutable.js和immer.js的区别

immutable.js

  • 自己维护了一套数据结构,Javascript 的数据类型和 immutable.js 的类型需要相互转换,对数据有侵入性。
  • 库的体积比较大(63KB),不太适合包体积紧张的移动端。
  • API 极其丰富,学习成本较高。
  • 兼容性非常好,支持 IE 较老的版本。

immer.js

  • 使用 Proxy 实现,兼容性差。
  • 体积很小(12KB),移动端友好。
  • API 简洁,使用 Javascript 自己的数据类型,几乎没有理解成本。

学习参考:

posted @ 2022-11-07 13:53  笔下洛璃  阅读(433)  评论(0编辑  收藏  举报