为什么不能直接修改React的State

2,091 阅读3分钟

react开发者都说不要这样做永远不要直接修改组件内的状态,而是调用setState

但是为什么呢?

如果你试过直接修改组件state的话,你可能注意到这样做好像没有出现什么问题。无论你直接修改组件state,还是调用 this.setState({}) 修改,亦或是使用 this.forceUpdate() 强制更新,似乎React渲染都是正常的,没发生什么问题。如下方式:

this.state.cart.push(item.id);
this.setState({ cart: this.state.cart });
// 可能这种方式渲染是正常的?

上面的例子直接修改了组件内部的state,这很不好,其中有两个原因(即使上面的例子或者更多其他的例子会正常工作)。

(其他需要避免使用的模式如 this.state.something = xthis.state = x

直接改变state可能导致一些奇怪的bug,并且使用该方式的组件很难进行优化。下面是一个例子。

React的开发者都知道,一种提高React组件性能的普通方式就是让这个组件变“纯(pure)”,纯组件(React.PureComponent)只会在传递给它的props变化时才重新渲染(而不是只要其父组件重新渲染,它就重新渲染)。有两种方式让组件变“纯”:一是继承 React.PureComponent 而不是 React.Component 来自动实现;二是手动覆盖生命周期方法shouldComponentUpdate ,通过比较当前props和nextProps 决定是否重新渲染。如果props没有变化,跳过本次渲染,从而节省渲染时间。

这里有一个简单的渲染一个列表的例子(注意列表组件继承自React.PureComponent):

class ItemList extends React.PureComponent {
  render() {
    return (
      <ul>
        {this.props.items.map(item => <li key={item.id}>{item.value}</li>)}
      </ul>
    );
  }
}

现在,这里有一个小的React应用渲染了ItemList组件,并增加了两个点击后增加列表项的按钮——一个通过this.setState()实现(不可变的方式),另一个通过直接修改地方式实现(可变的state)。看看会发生什么。

class App extends Component {
  // 初始化一个空的列表
  state = {
    items: []
  };

  // 初始化一个会自增的计数器
  // 用于为每个列表项赋ID值
  nextItemId = 0;

  makeItem() {
    // 创造一个新的列表项并使用
    // 列表项中的value值为随机小数
    return {
      id: this.nextItemId++,
      value: Math.random()
    };
  }

  // 正确的修改列表的方式:
  // 复制原始列表,添加一个新列表项后创建新的列表
  addItemImmutably = () => {
    this.setState({
      items: [...this.state.items, this.makeItem()]
    });
  };

  // 错误的修改列表的方式:
  // 直接修改原始列表,并且以可变模式设置回state中
  addItemMutably = () => {
    this.state.items.push(this.makeItem());
    this.setState({ items: this.state.items });
  };

  render() {
    return (
      <div>
        <button onClick={this.addItemImmutably}>
          Add item immutably (good)
        </button>
        <button onClick={this.addItemMutably}>Add item mutably (bad)</button>
        <ItemList items={this.state.items} />
      </div>
    );
  }
}

试试看

点击不可变模式下的增加按钮,看到了列表如预期一样增加了表项

然后点击可变模式下的增加按钮,注意到没有任何新的表项增加,即使state已经被改变了

最后,再次点击不可变增加按钮,会发现之前点击的可变模式按钮增加的表项会被重新渲染增加到列表中。

出现这样的情况是因为ItemList是纯组件,且每当通过this.state.items增加一个新表项并没有替换原本的数组。当React想要重新渲染ItemList时,shouldComponentUpdate 会注意到可变的数据结构(数组)的引用没有变化,所以没有重新渲染。

image.png

注:实际编写了例子才能理解其中原理,译者参考作者的例子写的源代码

总结

好了,这就是为什么即使立即调用了setState也不应该改变state的原因:这样做,优化的组件可能不会重新渲染,并且渲染出现的错误将难以追踪。

相反,调用setState一直会创建新的对象和数组,上面的例子中调用 setState 时使用扩展({})运算符保证了这一点。点击学习更多的如何使用扩展运算符更新不可变数据结构