为什么我不能直接修改组件的状态,真的吗?

IT技术 javascript reactjs
2021-01-15 00:49:07

我知道 React 教程和文档明确警告状态不应该直接改变,一切都应该通过setState

我想明白为什么,确切地说,我不能直接改变状态,然后(在同一个函数中)调用this.setState({})只是为了触发render.

例如:下面的代码似乎工作得很好:

const React = require('react');

const App = React.createClass({
  getInitialState: function() {
    return {
      some: {
        rather: {
          deeply: {
            embedded: {
              stuff: 1,
            },
          },
        },
      },
    },
  };
  updateCounter: function () {
    this.state.some.rather.deeply.embedded.stuff++;
    this.setState({}); // just to trigger the render ...
  },
  render: function() {
    return (
      <div>
        Counter value: {this.state.some.rather.deeply.embedded.stuff}
        <br></br>
        <button onClick={this.updateCounter}>Increment</button>
      </div>
    );
  },
});

export default App;

我完全支持遵循约定,但我想进一步了解 ReactJS 的实际工作方式以及可能出错的地方,或者上面的代码是否不是最理想的。

this.setState文档下的注释基本上确定了两个陷阱:

  1. 如果你直接改变状态然后随后调用this.setState它可能会替换(覆盖?)你所做的改变。我在上面的代码中看不到这是如何发生的。
  2. setState可能会this.state以异步/延迟方式有效地变异,因此this.state在调用后this.setState立即访问时,不能保证访问最终的变异状态。我明白了,如果this.setState是更新函数的最后一次调用,这不是问题
6个回答

这个答案是提供足够的信息来不直接在 React 中改变/改变状态。

React 遵循单向数据流意思是,react 内部的数据流应该并且预计将处于循环路径中。

React 的无流量数据流

在此处输入图片说明

为了让 React 像这样工作,开发人员使 React 类似于函数式编程函数式编程的经验法则是不变性让我大声而清楚地解释一下。

单向流是如何工作的?

  • states 是包含组件数据的数据存储。
  • 所述view一个部件的渲染基于状态。
  • view需要更改屏幕上的某些内容时,应从store.
  • 为了实现这一点,React 提供setState()了一个函数,它接收一个object或 newstates并对object.assign()之前的状态进行比较和合并(类似于),并将新状态添加到状态数据存储中。
  • 每当 state store 中的数据发生变化时,react 将触发使用新状态重新渲染view并在屏幕上显示它。

这个循环将在组件的整个生命周期中持续。

如果你看到上面的步骤,它清楚地表明当你改变状态时,后面发生了很多事情。因此,当您直接改变状态并setState()使用空对象进行调用时。previous state会与你的基因突变被污染。因此,两个状态的浅比较和合并将受到干扰或不会发生,因为您现在只有一个状态。这将破坏 React 的所有生命周期方法。

因此,您的应用程序将出现异常甚至崩溃。大多数情况下,它不会影响您的应用程序,因为我们用于测试的所有应用程序都非常小。

JavaScriptObjects突变的另一个缺点Arrays是,当您分配对象或数组时,您只是在引用该对象或该数组。当您改变它们时,对该对象或该数组的所有引用都会受到影响。React 在后台以一种智能的方式处理这个问题,并简单地给我们一个 API 来让它工作。

在 React 中处理状态时最常见的错误

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// bad approach
const b = this.state.a.push(6)
this.setState({
  a: b
}) 

在上面的例子中,this.state.a.push(6)将直接改变状态。将它分配给另一个变量并调用setState与下面显示的相同。无论如何,当我们改变状态时,将它分配给另一个变量并setState使用该变量调用是没有意义的

// same as 
this.state.a.push(6)
this.setState({})

很多人都这样做。这太错误了这打破了 React 的美感,是糟糕的编程习惯。

那么,在 React 中处理状态的最佳方法是什么?让我解释。

当您需要更改现有状态中的“某物”时,首先从当前状态中获取该“某物”的副本。

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]

现在,变异currentStateCopy不会改变原始状态。执行操作currentStateCopy并使用 将其设置为新状态setState()

currentStateCopy.push(6)
this.setState({
  a: currentStateCopy
})

这很漂亮,对吧?

通过这样做,在this.state.a我们使用setState. 这使您可以控制您的代码,这将帮助您编写优雅的测试并让您对代码在生产中的性能充满信心。

为了回答你的问题,

为什么我不能直接修改组件的状态?


是的,你可以但是,您需要面对以下后果。

  1. 扩展时,您将编写难以管理的代码。
  2. 您将失去对state跨组件的控制
  3. 您将在 React 上编写自定义代码,而不是使用 React。

不变性不是必需的,因为 JavaScript 是单线程的。但是,遵循实践是一件好事,从长远来看,这将对您有所帮助。

附注。我已经写了大约 10000 行可变的 React JS 代码。如果它现在坏了,我不知道去哪里查看,因为所有值都在某处发生了变异。当我意识到这一点时,我开始编写不可变代码。相信我!这是您可以对产品或应用程序做的最好的事情。

希望这可以帮助!

提醒一下:在 JS 中克隆的大多数基本方法(slice、ES6 解构等)都是浅薄的。如果您有嵌套数组或嵌套对象,则需要查看其他深度复制方法,例如JSON.parse(JSON.stringify(obj))(尽管如果您的对象具有循环引用,则此特定方法将不起作用)。
2021-03-22 00:49:07
@Galen Long 就_.cloneDeep足够了
2021-03-24 00:49:07
Pranesh,一个很好的答案。对于后续读者,在该行的第一个代码示例中,const b = this.state.a.push(6)常量 b 的值为6,因为Array.prototype.push()将返回数组新长度,而不是数组本身。所以记住这一点。此外,当this.setState({a: b})被执行时,state.a肯定指向“数字”(值)类型,而不是之前的“数组”(引用)类型。所以这已经改变了,但是对state对象本身的引用并没有改变,因此我们改变了状态。
2021-03-28 00:49:07
@JoshBroadhurst 很好,是的。如果有人担心只为一个功能安装整个 Lodash 包,您可以只安装 cloneDeep modulenpm install lodash.clonedeep并与let cloneDeep = require("lodash.clonedeep");.
2021-04-07 00:49:07

React 文档中setState有这样的说法:

切勿this.state直接变异,因为setState()之后调用可能会替换您所做的变异。将其this.state视为不可变的。

setState()不会立即变异,this.state而是创建一个挂起的状态转换。this.state调用此方法后访问可能会返回现有值。

无法保证调用的同步操作,setState并且可能会批处理调用以提高性能。

setState()除非在shouldComponentUpdate(). 如果正在使用可变对象并且无法在 中实现逻辑shouldComponentUpdate(),则setState()仅在新状态与先前状态不同时调用将避免不必要的重新渲染。

基本上,如果您this.state直接修改,则会造成这些修改可能被覆盖的情况。

与您的扩展问题 1) 和 2) 相关,setState()不是直接的。它根据它认为正在发生的事情对状态转换进行排队,这可能不包括对 的直接更改this.state由于它是排队而不是立即应用,因此完全有可能在两者之间修改某些内容,从而覆盖您的直接更改。

如果不出意外,考虑到不直接修改this.state可以被视为良好实践,您可能会更好您可能个人知道您的代码与 React 交互的方式不会发生这些覆盖或其他问题,但是您正在创造一种情况,其他开发人员或未来的更新可能会突然发现自己遇到奇怪或微妙的问题。

“它根据它认为正在发生的事情对状态转换进行排队,这可能不包括对 this.state 的直接更改。由于它是排队而不是立即应用,因此完全有可能在两者之间修改某些内容,以便您的直接更改被覆盖.” 你能详细说明你的意思吗?举一个例子,这是一个问题?
2021-03-14 00:49:07
感谢您的提示 Ouroborus!
2021-03-21 00:49:07
因为这个原因被赞成:“它根据它认为正在发生的事情对状态转换进行排队,这可能不包括对 this.state 的直接更改”
2021-03-29 00:49:07
@jhegedus深入 React 代码库:处理状态更改非常详细地介绍了setState()回调系统如何工作,并让您很好地了解为什么会出现问题。
2021-03-30 00:49:07

最简单的答案“

为什么我不能直接修改组件的状态:

都是关于更新阶段。

当我们更新组件的状态时,它的所有子组件也将被渲染。或者我们渲染的整个组件树。

但是当我说我们的整个组件树都被渲染时,并不意味着整个 DOM 都被更新了。当一个组件被渲染时,我们基本上会得到一个 react 元素,所以它正在更新我们的虚拟 dom。

React 然后会查看虚拟 DOM,它也有旧虚拟 DOM 的副本,这就是为什么我们不应该直接更新状态,所以我们可以在内存中有两个不同的对象引用,我们有旧虚拟 DOM 作为以及新的虚拟 DOM。

然后 react 会找出发生了什么变化,并根据它会相应地更新真实的 DOM。

希望能帮助到你。

为了避免每次创建副本,this.state.element您可以使用update with$set or $push或来自immutability-helper 的许多其他人

例如:

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

令我惊讶的是,当前的答案都没有谈论纯/备忘录组件。这些组件仅在检测到props之一发生变化时才重新渲染。

假设您直接改变状态并将其传递给下面的组件,而不是值,而是过度耦合对象。该对象仍然具有与前一个对象相同的引用,这意味着即使您改变了其中一个属性,pure/memo 组件也不会重新渲染。

由于在从库中导入组件时,您并不总是知道正在使用哪种类型的组件,因此这是坚持非变异规则的另一个原因。

这是此行为的示例(R.evolve用于简化创建副本和更新嵌套内容):

class App extends React.Component {
  state = { some: { rather: { deeply: { nested: { stuff: 1 } } } } };
  
  mutatingIncrement = () => {
    this.state.some.rather.deeply.nested.stuff++;
    this.setState({});
  }
  nonMutatingIncrement = () => {
    this.setState(R.evolve(
      { some: { rather: { deeply: { nested: { stuff: n => n + 1 } } } } }
    ));
  }

  render() {
    return (
      <div>
        Pure Component: <PureCounterDisplay {...this.state} />
        <br />
        Normal Component: <CounterDisplay {...this.state} />
        <br />
        <button onClick={this.mutatingIncrement}>mutating increment</button>
        <button onClick={this.nonMutatingIncrement}>non-mutating increment</button>
      </div>
    );
  }
}

const CounterDisplay = (props) => (
  <React.Fragment>
    Counter value: {props.some.rather.deeply.nested.stuff}
  </React.Fragment>
);
const PureCounterDisplay = React.memo(CounterDisplay);

ReactDOM.render(<App />, document.querySelector("#root"));
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/ramda@0/dist/ramda.min.js"></script>
<div id="root"></div>