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

我理解,反应教程和文档警告在 没有不确定的条件状态不应该直接突变,一切都应该通过 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.setState之后立即访问 this.state时,你不能保证访问最终的突变状态。我明白了,如果 this.setState是 update 函数的最后一次调用,这就不是问题了。
63199 次浏览

setState 的反应文档这样写道:

千万不要直接突变 this.state,因为之后调用 setState()可能会取代你所做的突变。对待 this.state就像它是不可变的。

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

不能保证对 setState的调用是同步操作的,为了提高性能,可能会对调用进行批处理。

除非在 shouldComponentUpdate()中实现了条件呈现逻辑,否则 setState()将始终触发重新呈现。如果使用的是可变对象,而且逻辑无法在 shouldComponentUpdate()中实现,那么只有在新状态与前一状态不同时调用 setState()才能避免不必要的重新呈现。

基本上,如果直接修改 this.state,就会创建这些修改可能被覆盖的情况。

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

如果没有其他事情的话,考虑到不直接修改 this.state可以被看作是一个好的实践,您可能会更好。您个人可能知道,您的代码与 React 的交互方式使得这些重写或其他问题不会发生,但是您正在创建一种情况,其他开发人员或未来的更新可能会突然发现自己存在奇怪或微妙的问题。

我目前的理解是基于 这个这个的答案:

如果 你的 不要使用 shouldComponentUpdate或任何其他生命周期方法(如 componentWillReceivePropscomponentWillUpdatecomponentDidUpdate) ,你比较旧的和新的 props/state

那么

突变 state然后调用 setState()是可以的,否则就不可以。

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

反应遵循 单向数据流 。这意味着,内部的数据流应该和将预期在一个循环的路径反应。

无流量的数据流

enter image description here

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

单向流是如何工作的?

  • states是一个包含组件数据的数据存储区。
  • 组件的 view基于状态呈现。
  • view需要更改屏幕上的某些内容时,该值应该由 store提供。
  • 为了实现这一点,React 提供了 setState()函数,该函数接受新的 statesobject,并在前一个状态上进行比较和合并(类似于 object.assign()) ,然后将新的状态添加到状态数据存储中。
  • 无论何时状态存储中的数据发生变化,response 都会触发一个重新呈现,其中包含 view使用的新状态,并将其显示在屏幕上。

这个周期将贯穿于组件的整个生命周期。

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

因此,您的应用程序将表现异常,甚至崩溃。大多数时候,它不会影响你的应用程序,因为我们用来测试它的所有应用程序都非常小。

在 JavaScript 中变异 ObjectsArrays的另一个缺点是,当你分配一个对象或数组时,你只是在引用那个对象或数组。当您对它们进行变异时,对该对象或该数组的所有引用都将受到影响。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 状态的最佳方法是什么呢? 让我来解释一下。

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

// 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
})

很漂亮,对吧?

通过这样做,在使用 setState之前,this.state.a的所有引用都不会受到影响。这使您可以控制自己的代码,并帮助您编写优雅的测试,使您对生产环境中代码的性能有信心。

回答你的问题,

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


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

  1. 当您进行伸缩时,您将编写难以管理的代码。
  2. 您将失去对 state跨组件的控制。
  3. 代替使用 React,您将在 React 上编写自定义代码。

不可变性不是必需的,因为 JavaScript 是单线程的。但是,这是一个很好的遵循实践,这将有助于你在长期。

附言。我已经写了大约10000行可变的 React JS 代码。如果现在出现问题,我不知道该从哪里着手,因为所有的价值观都在某个地方发生了变化。当我意识到这一点时,我开始编写不可变代码。相信我!这是你能对产品或应用程序做的最好的事情。

希望这个能帮上忙!

最简单的答案

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

就是 更新阶段。

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

但是当我说我们的整个组件树被渲染时,并不意味着整个 DOM 被更新了。 当一个组件被渲染时,我们基本上得到了一个 response 元素,这就是更新我们的虚拟空间。

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

然后,response 将找出哪些内容发生了更改,并根据这些内容相应地更新实际的 DOM。

希望能有所帮助。

为了避免每次创建 this.state.element的副本,您可以使用 更新$set or $push或许多其他来自 不变性助手的副本

例如:

import update from 'immutability-helper';


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

SetState 触发器重新呈现组件。当我们想要一次又一次地更新 state 时,我们必须使用 setState,否则它就不能正常工作。

令我惊讶的是,当前的答案都没有涉及纯/备忘录组件(React.PureComponentReact.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"));
<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>