首页 > 解决方案 > 带有自定义函数的 Redux、mutation 和 deepMerge

问题描述

在这里使用 Redux。我想使用胖动作,即在我的动作中包含所有可能的逻辑——以及 thunk——,并使减速器尽可能小。

为了实现这一点,我只有一个减速器,例如:

const types = {
  ADD_ARTICLE: 'ADD_ARTICLE',
  REMOVE_ARTICLE: 'REMOVE_ARTICLE'
};

const initialState = {
  articles: {
    byKey: {},
    byId: [],
  },
};

const ArticlesReducer = (state = initialState, action) => {
  return Object.hasOwnProperty.call(types, action.type)
    ? Object.assign({}, state, action.payload)
    : state;
};

现在一个动作可以像

export const addArticle = () => ({
  type: types.ADD_ARTICLE,
  payload: {
    byKey: {
      [1]: {
        id: 1,
        title: 'My first article',
      },
    },
  },
});

并且 reducer 会自动更新Articles.byKey. 但现在我添加另一篇文章:

export const addArticle = () => ({
  type: types.ADD_ARTICLE,
  payload: {
    byKey: {
      [2]: {
        id: 2,
        title: 'My second article',
      },
    },
  },
});

而reducer,就像使用Object.assign一样,只是删除了第一篇文章。所以我需要做一个深度合并。为此,我创建了一个自定义合并函数:

const isObject = (item) =>
  item && typeof item === "object" && !Array.isArray(item);
const isArray = (item) =>
  item && typeof item === "object" && !!Array.isArray(item);

// Deep merge Objects and Arrays
// If we want to remove elements, assign them as `null` or `undefined`
const mergeDeep = (
  target,
  sources,
  replaceEmptyArrays = false,
  replaceEmptyObjects
) => {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      // Object case
      if (isObject(source[key])) {
        // If is an object, deep merge it
        if (!target[key]) {
          // If there is no current key in the target, create it and skip
          Object.assign(target, { [key]: source[key] });
        } else if (!!replaceEmptyObjects) {
          // If there is key, and we want to replace it, do Object.assign
          Object.assign(target, { [key]: source[key] });
        } else {
          // If there is key, and we want do deepMerge it, do recursive mergeDeep
          mergeDeep2(target[key], [source[key]], { replaceEmptyArrays, replaceEmptyObjects });
        }
      }

      // Array case
      if (isArray(source[key])) {
        if (replaceEmptyArrays) {
          // If is an array, and we DO NOT want to deep merge it with `!!replaceEmptyArrays` option—, we object.assign it, replacing it if empty
          Object.assign(target, { [key]: source[key] });
        } else {
          // If is an array, and we want to deep merge it —by default— with `!replaceEmptyArrays` option—, we merge it and remove duplicates with a Set
          Object.assign(target, {
            [key]: Array.from(new Set([...target[key], ...source[key]])),
          });
        }
      }

      // Neither Object nor Array casess
      if (!isObject(source[key]) && !isArray(source[key])) {
        // Simply replace it with Object.assign
        Object.assign(target, { [key]: source[key] });
      }
    }
  }

  return Object.assign({}, mergeDeep2(target, [...sources], { replaceEmptyArrays, replaceEmptyObjects }));
};

它有效:

const target = {
  a: 1,
  b: {
    c: 1,
    d: {
      e: {
        f: 1,
        g: 2,
      },
    },
  },
};

const source = {
  a: 1,
  b: {
    c: 1,
    d: {
      e: {
        h: 3,
      },
    },
    j: {
      k: 1,
    },
  },
};

const expectedResult = {
  a: 1,
  b: {
    c: 1,
    d: {
      e: {
        f: 1,
        g: 2,
        h: 3,
      },
    },
    j: {
      k: 1,
    },
  },
};

console.log("=======");
console.log("expectedResult:");
console.log(JSON.stringify(expectedResult, null, 4));
console.log("--------------------------------------------------------");
console.log("mergeDeep(target, [source]):");
console.log(JSON.stringify(mergeDeep(target, [source]), null, 4));
console.log("=======");

但是现在,回到 Redux,当我在 reducer 中使用它时:

const ArticlesReducer = (state = initialState, action) => {
  return Object.hasOwnProperty.call(types, action.type)
    ? mergeDeep(state, [action.payload])
    : state;
};

Redux 不会更新 UI。这意味着存在突变:但是,如果我的合并函数返回带有 Object.assign 的合并对象,那怎么可能呢?

[…]
return Object.assign({}, mergeDeep2(target, [...sources], { replaceEmptyArrays, replaceEmptyObjects }));

我可以将标志设置replaceEmptyObjectstrue,redux 将再次工作:

const ArticlesReducer = (state = initialState, action) => {
  return Object.hasOwnProperty.call(types, action.type)
    ? mergeDeep(state, [action.payload], {
        replaceEmptyObjects: true,
      })
    : state;
};

但我根本不会是一个深度合并。

我可以使用 lodash 合并,它可以深度合并——或某种——;但我想控制我的对象合并的方式,并理解为什么会发生这种情况。

标签: javascriptobjectredux

解决方案


推荐阅读