避免递归函数中的状态突变(Redux)

Avoiding state mutation in a recursive function (Redux)

我正在使用 React 和 Redux 构建一个网络应用程序,我遇到了一个关于状态不变性的问题。

我的状态类似于:-

{
    tasks: [
        {
            id: 't1',
            text: 'Task 1',
            times: {
                min: 0,
                max: 0
            },
            children: [
                { id: 't1c1', text: 'Task 1 child 1', times: { min: 2, max: 3 }, children: [] },
                { id: 't1c2', text: 'Task 1 child 2', times: { min: 3, max: 4 }, children: [] }
                ...
            ]
        }
        ...
    ]
}

这定义了 "task" 对象的层次结构,每个任务在 "children" 数组中具有零个或多个子任务。每个任务还​​有一个 "times" 对象,它记录了该任务将花费的最小 ("min") 和最大 ("max") 时间(以小时为单位)。

我创建了一个简单的递归函数,用于遍历任务层次结构并计算和设置具有一个或多个子任务的所有任务的总 "times" 值(最小值和最大值)。所以在上面的例子中,任务 "t1" 在计算后应该有 5 (2 + 3) 的最小时间和 7 (3 + 4) 的最大时间。这个函数当然需要在本质上是递归的,因为每个任务都可以有任意数量的祖先。

(以下是凭记忆写的,没有测试所以请 ignore/forgive 如有错别字)

function taskTimes(tasks)
{
    let result = { min: 0, max: 0 };

    if (tasks.length === 0)
        return result;

    tasks.forEach(task => {
        if (task.children.length > 0)
        {
            let times = taskTimes(task.children);
            task.times.min = times.min; // MUTATING!
            task.times.max = times.max; // MUTATING!
            result.min += times.min;
            result.max += times.max;
        }
        else
        {
            result.min += task.times.min;
            result.max += task.times.max;
        }
    });

    return result;
}

我创建的函数工作正常,但我在编写它之前就知道我会遇到一个问题:在遍历任务层次结构时,单个任务对象(特别是 "times" 对象)发生了变化。这是一个问题,因为我想在减速器中 运行 这个,因此我想从这个动作创建一个新状态而不是改变它。

因此,我的问题是,修改此系统以避免改变状态的最佳方法是什么?

我可以尝试使用 Immutable.js 来强制整个层次结构的不变性,尝试这可能是我的下一步。另一种选择是根本不存储此数据,而是在必要时在每个组件中计算它。虽然后者可能是一个更简洁的选择,但我仍然想知道如何最好地解决这个问题,因为我可以预见必须在其他项目中做类似的事情,为了安心,我现在想解决这个问题。

如果您能就此领域的最佳实践提出任何建议,我们将不胜感激。另外,如果我忽略了一些非常明显的东西,请善待! 提前致谢。

你的第二个假设是正确的。您应该在商店中存储尽可能少的数据。可以计算的数据应该即时计算。如果您正在进行一些繁重的计算(如上面的递归计算),您应该看看 redux-reselect,这是一个小型库,用于创建记忆化选择器以计算来自商店的派生数据。

如果您的目的不是操纵状态,那么创建一个副本将是一个快速而肮脏的解决方法,如果您在计算结果时操纵它并不重要。 如果您打算使用该函数更新子任务的所有时间,一种简单的方法是创建状态的(深)副本,对其进行操作和return。

例如reducer的片段:

...
switch(action.type) {
  case actions.RECALCULATE_TASK_COUNT:
    var nextState = $.extend({}, state);
    taskTimes(nextState);
    return nextState;
  ...

}
...

function taskTimes(tasks)
{
    let result = { min: 0, max: 0 };

    if (tasks.length === 0)
        return result;

    tasks.forEach(task => {
        if (task.children.length > 0)
        {
            let times = taskTimes(task.children);
            task.times.min = times.min; // MUTATING!
            task.times.max = times.max; // MUTATING!
            result.min += times.min;
            result.max += times.max;
        }
        else
        {
            result.min += task.times.min;
            result.max += task.times.max;
        }
    });

    return result;
}

首先,感谢 Pcriulan 提供了正确解决此问题的答案,并感谢 Deton 提供了理论问题的可能解决方案。

我想我会分享我创建的最终解决方案。正如 Pcriulan 指出的那样,这可能不是在实际应用程序中解决此问题的最佳方法,但我想在此处展示此解决方案,因为它解决了理论问题。

所以我的解决方案涉及修改递归函数,使其以类似于 reducer 本身的方式运行,特别是它将 return 任务数组(和子数组)的副本或未修改的任务对象,基于它们的属性是否被修改。因此,它只提供修改过的对象的深拷贝,效率应该是比较高的。我的单元测试还表明状态现在没有被函数改变,因此解决了最初的问题。

更新后的函数为:-

function getTaskTime(tasks)
{
    let result = { min: 0, max: 0, tasks: [] };

    if (tasks.length === 0)
        return result;

    // Build a new tasks array
    result.tasks = tasks.map(task => {

        if (task.children.length > 0)
        {
            // Get times and new task array for task.children
            const child_result = getTaskTime(task.children);

            // Add child times to current totals
            result.min += child_result.min;
            result.max += child_result.max;

            // Return new task object with new child array and modified times
            return Object.assign({}, task, {
                time: { min: child_result.min, max: child_result.max },
                children: child_result.tasks
            });
        }
        else
        {
            // If task has no children, simply add times to current totals and return
            // the unmodified task object
            result.min += task.time.min;
            result.max += task.time.max;
            return task;
        }

    });

    return result;
}

希望这能帮助其他面临类似情况的人。如果有任何改进或遗漏的问题,请随时发表评论。