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

时间:2016-06-14 18:12:22

标签: javascript recursion reactjs immutability redux

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

我的州看起来与此类似: -

{
    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: [] }
                ...
            ]
        }
        ...
    ]
}

这定义了"任务"的层次结构。具有零个或多个子任务的每个任务的对象在" children"阵列。每项任务还有一个"次"记录此任务将采用的最小(" min")和最大(" max")次(以小时为单位)的对象。

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

(以下内容是在未经测试的情况下从内存中编写的,请忽略/原谅任何拼写错误)

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"对象)会发生变异。这是一个问题,因为我想在reducer中运行它,因此我想从这个动作创建一个新的状态而不是改变它。

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

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

非常感谢任何有关该领域最佳实践的建议。另外,如果我忽略了一些非常明显的事情,请善待! 提前谢谢。

3 个答案:

答案 0 :(得分:2)

如果你的意图不是操纵状态,那么它将是一个快速的&脏的解决方法来创建一个副本,如果你在计算结果时操作它并不重要。 如果您打算使用该函数更新子任务的所有时间,则一种简单的方法是创建状态的(深层)副本,操作并返回它。

例如缩减器的片段:

...
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;
}

答案 1 :(得分:0)

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

答案 2 :(得分:0)

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

我以为我会分享我创建的最终解决方案。正如Pcriulan所指出的,这可能不是在现实世界中应用的最好方法,但是我想在这里展示这个解决方案,因为它解决了理论问题。

所以我的解决方案涉及修改递归函数,使其以与reducer本身类似的方式运行,特别是它将返回基于的新的或未修改的任务对象的tasks数组(和子数组)的副本。他们的属性是否被修改。因此,它仅提供已修改对象的深层副本,因此它应该相对有效。我的单元测试还表明状态现在没有被函数变异,因此解决了最初的问题。

更新的功能是: -

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;
}

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