Git Merge合并的那些坑我帮你踩过了

爱学习的尚文 工具 阅读 1,187
赞 24 收藏
二维码
手机扫码查看
反馈

merge搞不定,数据覆盖把我整懵了

今天遇到个蛋疼的问题,本来以为很简单的一个对象合并操作,结果被坑得够呛。项目里有个需求是动态更新用户配置,每次收到新数据的时候要把老配置和新配置合并,结果写出来的代码每次都是新数据完全替换掉老数据,根本不是我想要的深度合并效果。

Git Merge合并的那些坑我帮你踩过了

折腾了半天发现是 merge 方法没用对,或者说用错了工具。刚开始直接用 Object.assign(),看起来能跑,实际上深层嵌套的数据一合并就炸了。

Object.assign() 踩的第一个坑

先说说我最初的写法:

const oldConfig = {
  user: {
    name: '张三',
    profile: {
      age: 25,
      hobby: ['读书', '游戏']
    }
  },
  settings: {
    theme: 'dark'
  }
};

const newConfig = {
  user: {
    profile: {
      age: 26
    }
  }
};

// 错误的做法
const merged = Object.assign({}, oldConfig, newConfig);
console.log(merged);
// 结果 user.name 直接没了,整个 user 对象都被 newConfig.user 替换了

这里我踩了个大坑,Object.assign() 是浅拷贝,遇到嵌套对象的时候就会把整个对象替换成新的,而不是把里面的属性合并。所以 user 下面的所有属性都没了,只剩下一个 profile。

lodash.merge 确实靠谱,但项目不能引入库

后面我试了 lodash 的 merge 方法,确实好用:

import { merge } from 'lodash';

const oldConfig = {
  user: {
    name: '张三',
    profile: {
      age: 25,
      hobby: ['读书', '游戏']
    }
  }
};

const newConfig = {
  user: {
    profile: {
      age: 26,
      city: '北京'
    }
  }
};

const result = merge({}, oldConfig, newConfig);
console.log(result);
// user.name 保留了,profile.age 更新了,还加了 city 字段

但是客户那边比较严格,不让引入第三方库,所以这个方案 pass 掉了。

自己手写 deepMerge,边界情况真多

没办法只能自己手写了,网上找了个基础版本改了改,结果发现各种边界情况要考虑:

function deepMerge(target, source) {
  const output = { ...target };
  
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target)) {
          output[key] = source[key];
        } else {
          output[key] = deepMerge(target[key], source[key]);
        }
      } else {
        output[key] = source[key];
      }
    });
  }
  
  return output;
}

function isObject(item) {
  return item && typeof item === 'object' && !Array.isArray(item);
}

写完之后测试了一下,发现数组处理有问题。比如用户的 hobby 数组,我希望的是替换而不是合并,但上面的逻辑会把数组当普通对象来处理。后来改成这样:

function deepMerge(target, source) {
  const output = Array.isArray(target) ? [...target] : { ...target };
  
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target)) {
          // 如果目标不存在该属性,直接赋值
          output[key] = source[key];
        } else {
          // 如果都存在,递归合并
          output[key] = deepMerge(target[key], source[key]);
        }
      } else if (Array.isArray(source[key])) {
        // 数组直接替换,不合并
        output[key] = [...source[key]];
      } else {
        // 基本类型直接覆盖
        output[key] = source[key];
      }
    });
  }
  
  return output;
}

function isObject(item) {
  return item && typeof item === 'object' && !Array.isArray(item);
}

ES6 扩展运算符也不是万能的

一开始我还想着用扩展运算符一行搞定:

const merged = {...oldConfig, ...newConfig};

结果一样是浅合并的问题,深层对象还是会被整个替换掉。不过对于一层的对象确实挺方便的,就是不能处理复杂嵌套。

最终方案和注意事项

最后我用了改进版的 deepMerge 函数,但发现还有几个要注意的地方:

  • 循环引用要处理,不然会栈溢出
  • null 和 undefined 要特别判断
  • Date 对象要不要特殊处理
  • Symbol 类型也要考虑

这是最终用的版本:

function deepMerge(target, source) {
  // 防止循环引用
  if (target === source || source == null) return target;
  
  const output = Array.isArray(target) ? [...target] : { ...target };
  
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key]) && target[key]) {
        output[key] = deepMerge(target[key], source[key]);
      } else if (Array.isArray(source[key])) {
        output[key] = [...source[key]];
      } else if (source[key] !== undefined && source[key] !== null) {
        output[key] = source[key];
      }
    });
  } else if (source) {
    return source;
  }
  
  return output;
}

function isObject(item) {
  return item && typeof item === 'object' && !Array.isArray(item) && !(item instanceof Date);
}

// 测试一下
const config1 = {
  user: {
    name: '张三',
    profile: {
      age: 25,
      hobby: ['读书', '游戏'],
      address: {
        home: '北京',
        work: '朝阳'
      }
    }
  },
  settings: {
    theme: 'light'
  }
};

const config2 = {
  user: {
    profile: {
      age: 26,
      city: '上海',
      address: {
        home: '上海'
      }
    }
  },
  settings: {
    language: 'zh-CN'
  }
};

const result = deepMerge(config1, config2);
console.log(result);
// 现在能正常合并了,name 保留,age 更新,新增 city,home 地址也更新了

到这里基本解决了问题,不过还是有点瑕疵。比如如果新数据传过来某些字段是 null,现在是会覆盖掉原来的值,可能需要根据业务需求调整 null 值的处理逻辑。但总体来说能用,暂时先这么处理吧。

性能考虑和后续优化

后面我还注意到一个问题,就是每次合并都会创建新对象,频繁操作的话可能会有性能影响。但对于我这个项目的使用场景,配置更新频率不高,应该问题不大。如果后期发现性能瓶颈,可能要考虑用 immutable.js 或者类似的不可变数据结构库了。

还有一个小问题就是错误处理,现在的代码没有做太多异常处理,万一传入的数据格式不对可能会报错。不过考虑到数据来源可控,暂时先这样了。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论