为什么Redux中的对象应该是不可变的?

51
为什么Redux中的对象应该是不可变的? 我知道一些框架,比如Angular2将使用onPush并利用不可变性来比较视图状态以实现更快的渲染,但我想知道是否还有其他原因,因为Redux是框架无关的,但在自己的文档中提到要使用不可变性(无论框架如何)。
感谢任何反馈。

2
它使得推理更加简单,有助于防止您意外地在Redux之外更改状态。 - Matthew Herbst
3
谢谢您的回答,但有些含糊不清。我知道这是他们经常说的话。将名称从A更改为B并不会使其更复杂或更难理解。 - born2net
4
这样说吧:如果我知道一个数据结构是不可变的,那么我就知道代码中有一些部分绝对不能改变它。这使得测试代码和发现错误变得更加简单。 - Matthew Herbst
好的,对我来说,接口数据结构听起来更像是一个更可靠的合同,但这只是我的想法...无论如何,我将使用不变性作为模式,以便在ng2中获得更快的onPush渲染...谢谢。 - born2net
是的,JavaScript没有Java可以强制执行的所有好规则。ES6带来了很多结构,但归根结底,仍然很容易无意中轻松更改您不想更改的内容。 - Matthew Herbst
显示剩余2条评论
5个回答

48

Redux是一个小型的库,将状态表示为(不可变)对象。通过纯函数将当前状态传递来创建全新的对象/应用程序状态,从而产生新状态

如果您感到有些困惑,不用担心。简而言之,Redux不通过修改对象来表示应用程序状态的更改(如面向对象编程所做的那样)。相反,状态更改表示为输入对象和输出对象(var output = reducer(input))之间的差异。如果您改变中的任意一个,则会使状态无效。

用另一种方式来概括,Redux需要不可变性,因为它将您的应用程序状态表示为"冻结的对象快照"。通过这些离散的快照,您可以保存状态或撤消状态,并且通常对所有状态更改都有更多的"控制"。

你的应用程序的状态仅由被称为reducer的一类纯函数更改。Reducer具有两个重要属性:

  1. 它们永远不会改变状态,返回新构建的对象:这允许在没有副作用的情况下推理输入+输出
  2. 它们的签名总是function name(state, action) {},因此易于组合:

假设状态看起来像这样:

    var theState = {
      _2ndLevel: {
        count: 0
      }
    }

我们希望增加计数,因此我们创建了这些reducer

const INCR_2ND_LEVEL_COUNT = 'incr2NdLevelCount';

function _2ndlevel (state, action) {
    switch (action.type) {
        case INCR_2ND_LEVEL_COUNT:
            var newState = Objectd.assign({}, state);
            newState.count++
            return newState;
        }
    }

function topLevel (state, action) {
    switch (action.type) {
        case INCR_2ND_LEVEL_COUNT:
            return Object.assign(
                {}, 
                {_2ndLevel: _2ndlevel(state._2ndlevel, action)}
            );
    }
}

请注意在每个reducer中使用Object.assign({}, ...)来创建全新的对象:

假设我们已经将Redux连接到这些reducers,那么如果我们使用Redux的事件系统来触发状态更改...

    dispatch({type: INCR_2ND_LEVEL_COUNT})

...Redux 会调用:

    theNewState = topLevel(theState, action);

注意: action 是来自于 dispatch()

现在,theNewState 是一个全新的对象。

请注意:您可以使用(或新的语言特性)强制实现不可变性,或者只需小心不要改变任何东西:D

如果想深入了解,我强烈推荐您查看这个视频,由创建者Dan Abramov制作:video链接。它应该回答您所拥有的任何问题。


7
首先感谢您的回答,我点赞。但是让我解释一下,我非常清楚Redux的工作原理,我已经在使用它了,我的问题更深入一些。想想看,如果您只改变了名称而不是替换持有名称的整个对象,您仍然可以实现“冻结对象快照”。唯一的原因(这可能足够好的原因)是为了速度比较。因为当冻结状态时,比较新对象和更改过的对象可能会更快。那将是我能想到的唯一原因...谢谢Sean。 - born2net
2
哦,好的 - 糟糕 :P。嗯,我不明白当你改变一个名称时如何实现“冻结对象快照”,因为这样你就改变了对象。但无论如何,我认为你正走在正确的轨道上。对不变性的关注是因为在典型的SPA中发生的计算经济学倾向于浅层/愚蠢的对象比较重建状态。想象一下,你修改的不是名称,而是渲染表格中1000行中单个单元格中的名称。当“某些事情”发生变化时重新渲染表格比遍历整个状态以找出“确切变化”的代价更小。 - Ashley Coolman
9
我不认为这回答了“为什么状态应该是不可变的”这个问题。它确实解释了如何做到这一点,并重复了Redux规范:“状态应该是不可变的”,但没有描述/解释背后的动机。如果应用程序状态需要描述转换(更改)的任何原因,那么在任何纯函数调用之前和之后,只需进行Object.assign({}, ...)操作即可。 - Slaus
1
@AshleyCoolman - 所以,继续你上面的数组示例 - 如果我更改了单元格的值并返回相同的数组 - 组件是否不会重新绘制,还是库需要更长时间告诉哪些内容已更改?在后一种情况下,不可变性的要求更多是性能建议。 - Erez Cohen
2
@ErezCohen 如果您更改对象的内部,则该对象仍然是相同的 - 因此,Redux 进行的便宜比较不会注意到更改。 您需要返回一个新的顶级对象。 https://facebook.github.io/immutable-js/ 是强制执行此操作的一种方法。 - Ashley Coolman
显示剩余2条评论

16

Redux 文档中提到了以下不可变性的好处:

  • Redux 和 React-Redux 都采用浅相等检查。特别地:
    • Redux 的 combineReducers 工具浅层检查由其调用的 reducer 引起的引用更改。
    • React-Redux 的 connect 方法生成的组件浅层检查根状态和 mapStateToProps 函数的返回值,以查看是否需要重新渲染包装的组件。这种浅层检查需要使用不可变性才能正常工作。
  • 不可变数据管理最终使数据处理更加安全。
  • 时光旅行式调试要求 reducer 是纯函数,没有副作用,这样您就可以在不同的状态之间正确跳转。

换句话说,忽略建议并改变state会干扰“应该组件更新”的逻辑吗?能否说明会以何种方式出错,例如始终/从不/取决于什么? - bluenote10
1
你的问题的答案并不是简单的。它取决于你的状态如何结构化,你正在改变什么深层属性以及是否使用了 combineReducers 和/或 react-redux。如果你更深入地了解 combineReducersreact-redux,你肯定可以知道会发生什么。通常情况下,当由于浅层检查而未检测到深层更改时,你的 UI 中的某些组件将不会更新。但最重要的是——不要改变状态。这是一件坏事™。 - Andrea Casaccia
他们第一次复制了所有对象,然后说用“浅”比较会更快。哈哈,我开始打嗝了。 - puchu
@bluenote10,你可以自由地改变你的状态,不要听从函数式编程的狂热者。不可变性是一件与JS无关的神圣遗物。请阅读更多关于react+mobxvue.js的内容。 - puchu

9

Redux使用不可变性的主要原因是它不必遍历对象树来检查每个键值的更改。相反,它只会检查对象的引用是否已更改以便在状态更改时更新DOM。


这是不正确的,因为UI层仍然必须遍历整个对象树(即新状态)以查看发生了什么变化。 - user1034912

1

如果函数只处理不可变数据,那么它是纯的。但它是不可逆的。有一些纯函数会改变数据。可以从不纯的函数组创建纯函数。这个概念很古老,也不完整。不要毒害人们的大脑。 - puchu

0

根据官方文档:

在Redux中,有几个原因说明为什么不能改变状态:

  • 它会导致错误,例如UI无法正确更新以显示最新值
  • 它使得更难理解状态何时以及如何被更新
  • 它使得编写测试更加困难
  • 它破坏了正确使用“时间旅行调试”的能力
  • 它违背了Redux的预期精神和使用模式

Reudx官方文档


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接