从 React 的组件更新谈 Immutable 的应用

在上一篇文章《Immutable 在 JavaScript 中的应用》 中主要介绍了 Immutable 之于 JavaScript。而基于 Immutable 的特性,将其应用在 React 项目的开发中非常合适,解决了 React 中的一些痛点,能进一步提升 React 组件的性能以及更好的管理组件的状态。

在介绍 Immutable 如何在 React 中应用之前,先来谈谈 React 组件是如何更新的。

React 是基于状态驱动的开发,可以将一个组件看成是一个有限状态机,组件要更新,必须更新状态。

通常说的组件的状态就是组件的 state 对象,state 是可以由当前组件自行修改更新的,这种自更新的状态的为了便于理解区分可以称之为“动态”的状态。但除了更新 state 外,组件还可以通过 props 来更新,props 属性不能由组件自行修改,必须由父组件来修改,然后再传递给当前组件,更新组件的 props 也能引起组件的更新,可以将 props 称之为“静态”的状态。这样的状态区分是广义上的,如果你不认可 props 也是状态也没关系,这里可以不用拘泥于文字。

组件的更新说起来可能会显得抽象一点,实际上我们说要更新一个组件其实就是更新 DOM 树,React 设计的再牛逼,要在浏览器中跑起来最终还是要生成 DOM 树。在使用 React 时通常并不直接操作 DOM,而是由状态驱动,状态可以随时更新,而 DOM 树并不会随着状态的更新而更新,因为对于 DOM 的频繁的操作是很耗费性能的。为了节省性能,React 内部实现了一套 Virtual DOM (虚拟的 DOM),简言之就是将原生的 DOM 树通过 JavaScript 对象来做一次映射。状态的更新会引起 Virtual DOM 的更新,React 通过高效的 Diff 算法来比较状态更新前和更新后的 Virtual DOM 是否真的变化,只有 Virtual DOM 更新了才会更新真实的 DOM 树。

React组件的更新流程

只要状态更新,那么就一定会引起 Virtual DOM 的 Diff 操作,而原生 DOM 是否更新就要看 Diff 的结果。

如果要更新状态,无论是动态的 state 状态,还是静态的 props 状态,必然要主动调用 setState 方法,而且只要调用了该方法,那么就一定会更新状态。这也意味着每调用一次 setState 方法都会触发 Virtual DOM 的 Diff 操作,尽管可能并未更新原生 DOM,Diff 操作也会带来性能的开销。

每更新一次状态,组件都会执行 Update 的生命周期中的一系列方法:

componentWillReceiveProps shouldComponentUpdate componentWillUpdate componentDidUpdate

当然,还会执行 render 方法。

不过实际情况还要复杂些,只要一个组件更新了动态的 state 状态,那么这个组件包含的所有子组件以及子组件一层层嵌套的子组件都会执行上面的 Update 流程。

React的组件树的Render示意图

关于 setState 方法,调用之后并不会立即执行,而是会有一个事件循环,在这个循环中标记组件为 dirty 状态,等事件循环结束,才会将所有标记为 dirty 状态的组件执行 Update 的流程,此时才会重新 render 所有的子孙组件。所以 setState 是一个异步的操作。

React 的这种组件的更新方式,虽然用了很多办法来节省性能开销,诸如事件循环的机制、Virtual DOM、高性能的 Diff 算法,但仍然避免不了会有一些性能开销,并且随着组件的复杂度的提升对于性能的开销而成正比。

那么有没有办法对这种更新方式进行优化呢?先来看看 React 都提供了什么。在 Update 的生命周期阶段会触发一个 shouldComponentUpdate 的方法,在这个方法中可以主动的去 Diff 状态。

shouldComponentUpdate (nextProps, nextState) {
    return nextProps.id !== this.props.id;
};

shouldComponentUpdate 方法在执行完如果返回的是 true,那么组件就会继续 Update 的流程,如果返回 false,则不会继续 Update 的流程,默认情况下都是返回的 true。上面的方法判断的是两次的 id 是否相等,如果不相等才继续 Update 流程。

有没有觉得这样挺好的,好像有哪里不对啊,组件的状态我不可能都以硬编码的形式写上一大坨而且无法复用的代码,而且说好的 Immutable 也没见用上啊。

是的,上面说了那么一大堆,总算该轮到 Immutable 出场了。在进行状态的 Diff 时,对于复杂的 Mutable 数据,一项一项的去遍历不现实,借用 Immutable,可以直接实现「值」的比较,而且性能又好。

所有复杂的状态的 Diff,结合 Immutable,都能用下面这个工具方法全搞定。

import { is } from 'immutable';
const keys = Object.keys;

const shallowEqualImmutable = (context, nextProps, nextState) => {
    const currentState = context.state;
    const currentProps = context.props;
    const nextStateKeys = keys(nextState || {});
    const nextPropsKeys = keys(nextProps);

    // 先从数据的长度判断
    if (nextStateKeys.length !== keys(currentState || {}).length ||
        nextPropsKeys.length !== keys(currentProps).length
    ) {
        return true;
    }

    // 再按key逐个比较数据是否相等
    let isUpdate = nextStateKeys.some((item) => (
        currentState[item] !== nextState[item] &&
        !is(currentState[item], nextState[item])
    ));

    if (isUpdate) {
        return true;
    }

    return nextPropsKeys.some((item) => (
        currentProps[item] !== nextProps[item] &&
        !is(currentProps[item], nextProps[item])
    ));
};

那么组件中的 stateprops 状态数据,对于 Mutable 类型的数据都相应的都要转换成 Immutable 数据。

// 组件的初始 state 定义
state = {
    $list: Immutable.List([1, 2, 3]),
    foo: 'bar'
};

// 在某个方法中调用 setState
doSomething = () => {
    let $list = this.state.$list;
    $list = $list.push(4);
    
    this.setState({ $list });
};

// 调用 Diff 的工具方法
shouldComponentUpdate (nextProps, nextState) {
    return shallowEqualImmutable(this, nextProps, nextState);
};

将组件的 Mutable 状态都转换成 Immutable 后,组件的 Update 流程会变成如下所示,同时也避免了调用 setState 后,状态并未变化的带来的不必要的 Update 操作。

优化后的React的组件树的Render示意图

当父组件的 state 状态更新后,如果子组件的状态并未更新,那么子组件将不会再一层一层的去执行 Update 流程,从而达到优化性能的目的。

原载于:雨夜带刀’s Blog
本文链接:http://stylechen.com/react-and-immutable.html
如需转载请以链接形式注明原载或原文地址。

文章来源:

Author:admin
link:http://stylechen.com/react-and-immutable.html