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

Mar*_*tus 55 javascript reactjs

据我所知,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}>inc</button>
                </div>
        );
    }
});

export default App;
Run Code Online (Sandbox Code Playgroud)

我完全遵循以下约定,但我想进一步了解ReactJS如何实际运行,哪些可能出错或者是否与上述代码不是最佳的.

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

  1. 如果你直接改变状态然后调用this.setState它可能会替换(覆盖?)你所做的突变.我不知道在上面的代码中会发生这种情况.
  2. setState可能会this.state以异步/延迟的方式有效地改变,因此this.state在调用后this.setState立即访问时,您无法保证访问最终的突变状态.我知道,如果this.setState是更新功能的最后一次调用,这不是问题.

Pra*_*avi 37

这个答案是提供足够的信息,以便不直接在React中改变/改变状态.

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

React的数据流没有流量

在此输入图像描述

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

单向流如何工作?

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

该循环将在整个组件的整个生命周期内持续进行.

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

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

和突变的另一个缺点ObjectsArrays在JavaScript中,当您分配一个对象或数组,你要做的仅仅是该对象或数组的一个参考.当你改变它们时,对该对象或那个数组的所有引用都将受到影响.React在后台以智能方式处理它,并简单地为我们提供API以使其工作.

处理反应中的状态时最常见的错误

//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
}) 
Run Code Online (Sandbox Code Playgroud)

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

 //same as 
 this.state.a.push(6)
 this.setState({})
Run Code Online (Sandbox Code Playgroud)

大多数人这样做.这是错的.这打破了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]
Run Code Online (Sandbox Code Playgroud)

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

currentStateCopy.push(6)
this.state({
 a: currentStateCopy
})
Run Code Online (Sandbox Code Playgroud)

这美化吧?

通过这样做,所有引用this.state.a都不会受到影响,直到我们使用setState.这使您可以控制代码,这将帮助您编写优雅的测试,并使您对生产中的代码性能充满信心.

要回答你的问题,

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


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

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

不可变性不是必需的,因为Javascript是单线程的.但是,从长远来看,跟随练习会很有帮助.

PS.我写了大约10000行可变的React js代码.如果它现在中断了,我不知道在哪里查看,因为所有的值都在某处变异.当我意识到这一点时,我开始编写不可变代码.相信我!这是你可以对产品或应用程序做的最好的事情.

希望这可以帮助!

  • @JoshBroadhurst 很好,是的。如果有人担心只为一个功能安装整个 Lodash 包,您可以使用 `npm install lodash.clonedeep` 只安装 cloneDeep 模块,并将其与 `let cloneDeep = require("lodash.clonedeep");` 一起使用。 (3认同)
  • 只是提醒一下:JS(`slice`,ES6 destructuring等)中克隆的大多数基本方法都很浅薄.如果您有嵌套数组或嵌套对象,则需要查看其他深度复制方法,例如`JSON.parse(JSON.stringify(obj))`(尽管如果您的对象具有循环,此特定方法将不起作用引用). (2认同)
  • @Galen Long将[`_.cloneDeep`](https://lodash.com/docs/4.17.4#cloneDeep)满足 (2认同)

Our*_*rus 29

反应文档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交互的方式,使得这些覆盖或其他问题不会发生,但您正在创建一种情况,其他开发人员或未来的更新可能突然发现自己有奇怪或微妙的问题.

  • @jhegedus [深入 React 代码库:处理状态更改](http://reactkungfu.com/2016/03/dive-into-react-codebase-handling-state-changes/) 详细介绍了 `setState( )` 和回调系统工作,并让您很好地了解为什么会出现问题。 (3认同)

fas*_*sil 7

最简单的答案“

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

都是关于更新阶段。

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

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

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

然后 React 会找出发生了什么变化,并基于此更新真实的 DOM。

希望能帮助到你。


3li*_*t0r 5

令我惊讶的是,当前的答案都没有谈论 pure/memo 组件(React.PureComponentReact.memo)。这些组件仅在检测到其中一个道具发生变化时重新渲染。

假设您直接改变状态并将过度耦合对象传递给下面的组件,而不是值。该对象仍然具有与前一个对象相同的引用,这意味着 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>
        Normal Component: <CounterDisplay {...this.state} />
        <br />
        Pure Component: <PureCounterDisplay {...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"));
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)