【问题标题】:How to defensively prevent mutation of the Redux state?如何防御性地防止 Redux 状态的突变?
【发布时间】:2019-01-12 00:03:54
【问题描述】:

我正在尝试在我的应用程序中对减速器进行更具防御性的编码,因为我的团队使用预先存在的代码作为新更改的模板。出于这个原因,我试图涵盖所有与不变性的潜在偏差。

假设您有一个如下所示的初始状态:

const initialState = {
  section1: { a: 1, b: 2, c: 3 },
  section2: 'Some string',
};

还有一个 reducer 处理这样的动作:

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return { ...state, section1: action.payload.abc };
  }
  return state;
}

然后你可以有一个调度器来执行以下操作:

function foo(dispatch) {
  const abc = { a: 0, b: 0, c: 0 };
  dispatch({ type: SOME_ACTION, payload: { abc } });

  // The following changes the state without dispatching a new action
  // and does so by breaking the immutability of the state too.
  abc.c = 5;
}

在这种情况下,虽然 reducer 通过创建旧状态的浅表副本并仅更改更改的位来遵循不变性模式,但调度程序仍然可以访问 action.payload.abc 并可以对其进行变异。

也许 redux 已经创建了整个操作的深层副本,但还没有找到任何提到这一点的来源。我想知道是否有一种方法可以简单地解决这个问题。

【问题讨论】:

  • 您是否考虑过编写自己的基本中间件来处理它?
  • 我曾考虑创建一个中间件,在传递操作之前对其进行深度克隆,但想看看是否有更多现成的解决方案。不过,我描述的中间件听起来效率很低。

标签: javascript redux immutability


【解决方案1】:

应该注意的是,在您的示例中,如果您只是基于对象执行适当的级别复制,则突变不会引起任何问题。

对于看起来像 { a: 1, b: 2, c: 3 }abc,您可以只进行浅拷贝,而对于嵌套对象 { a: { name: 1 } },您必须对其进行深拷贝,但您仍然可以在没有任何库或任何东西的情况下显式执行此操作。

{
  ...state,
  a: {
    ...action.a
  }
}

或者,您可以使用eslint-plugin-immutable 防止突变,这将迫使程序员不要编写此类代码。

正如您在上面对 no-mutation rule 的 ESLint 插件的描述中看到的那样:

此规则与使用 Object.freeze() 防止 Redux reducer 发生突变一样有效。但是,此规则没有运行时成本。对象变异的一个很好的替代方法是使用 ES2016 中的对象扩展语法。


另一种相对简单的方法(不使用一些不变性库)来主动防止突变是在每次更新时冻结您的状态。

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return Object.freeze({
        ...state,
        section1: Object.freeze(action.payload.abc)
      });
  }
  return state;
}

这是一个例子:

const object = { a: 1, b: 2, c : 3 };
const immutable = Object.freeze(object);
object.a = 5;
console.log(immutable.a); // 1

Object.freeze 是一个浅层操作,因此您必须手动冻结对象的其余部分或使用像 deep-freeze 这样的库。

上述方法将保护突变的责任推给了您。

这种方法的一个缺点是,它通过明确的突变保护增加了程序员必要的认知努力,因此更容易出现错误(尤其是在大型代码库中)。

深度冻结时也可能会产生一些性能开销,尤其是在使用用于测试/处理您的特定应用可能从未引入的各种边缘情况的库时。


一种更具可扩展性的方法是将不可变模式嵌入到您的代码逻辑中,这会将编码模式自然地推向不可变操作。

一种方法是使用Immutable JS,其中数据结构本身的构建方式是对它们的操作总是会创建一个新实例并且永远不会发生变异。

import { Map } from 'immutable';

export default function(state = Map(), action) {
  switch(action.type) {
    case SOME_ACTION:
      return state.merge({ section1: action.payload.abc });
      // this creates a new immutable state by adding the abc object into it
      // can also use mergeDeep if abc is nested
  }
  return state;
}

另一种方法是使用immer,它在幕后隐藏不变性,并按照copy-on-write 原则为您提供可变的api。

import produce from 'immer'

export default = (state = initialState, action) =>
  produce(state, draft => {
    switch (action.type) {
      case SOME_ACTION:
        draft.section1 = action.section1;
    })
}

如果您正在转换一个可能有很多执行突变的代码的现有应用程序,这个库可能对您很有效。

不可变库的缺点是它增加了不熟悉该库的人进入代码库的障碍,因为现在每个人都必须学习它。

话虽如此,一致的编码模式通过明确限制代码的构建方式减少了认知工作(每个人都使用相同的模式)并减少了代码混乱因素(防止人们一直发明自己的模式) .这自然会导致更少的错误和更快的开发。

【讨论】:

  • 很遗憾,我无法投票,因为我刚刚创建了我的帐户并且尚未启用该功能,但是这个答案为我提供了许多研究和试用工具,我很感激他们。
  • 很高兴帮助队友,不用担心:)
【解决方案2】:

我是 Redux 维护者。

Redux 本身 does nothing to prevent mutations to your state。部分原因是 Redux 不知道也不关心实际状态是什么。它可以是单个数字、纯 JS 对象和数组、Immutable.js 映射和列表,或其他东西。

话虽如此,还有several existing development addons that catch accidental mutations

我会特别建议您试用我们的新redux-starter-kit package。现在,当您使用 configureStore() 函数时,它会默认将 redux-immutable-state-invariant 中间件添加到您的商店中,并且还会检查是否意外添加了不可序列化的值。此外,它的createReducer() 实用程序允许您定义通过“改变”状态来简化不可变更新逻辑的化简器,但更新实际上是不可变地应用的。

【讨论】:

    【解决方案3】:

    我相信在你的 reducer 中,如果你从 action.payload.abc 创建一个新对象,那么你可以确定对原始对象的任何更改都不会影响 redux 存储。

    case SOME_ACTION:
          return { ...state, section1: {...action.payload.abc}};
    

    【讨论】:

    • 对!我想在这里找到更通用的解决方案或方法。否则,代码模式并不清楚为什么有时会这样做,并且团队中的其他开发人员可能不记得做同样的事情。
    • 啊!在这种情况下,nem035 的回答非常有帮助
    猜你喜欢
    • 1970-01-01
    • 2022-01-25
    • 2012-01-31
    • 2017-12-10
    • 2011-01-15
    • 1970-01-01
    • 2011-05-03
    • 2023-03-08
    相关资源
    最近更新 更多