Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jest 源码解析 - 拷贝值 #19

Open
pikou1995 opened this issue Mar 18, 2021 · 1 comment
Open

jest 源码解析 - 拷贝值 #19

pikou1995 opened this issue Mar 18, 2021 · 1 comment

Comments

@pikou1995
Copy link
Owner

pikou1995 commented Mar 18, 2021

说到拷贝,我们熟知的场景有浅拷贝和深拷贝

// 浅拷贝
{...obj}
Object.assign({}, obj)

// 深拷贝
JSON.parse(JSON.stringify(obj))

以上方法,在 immutable 思想编程中基本已经足够使用了。

第二种深拷贝的,是有很多限制的。一是某些类型的值比如undefined, Date, function 等是并不能被序列化,而且处理不了对象的循环引用。

直到我 debug jest 的代码的时候,发现了一个 jest-matcher-utils 的模块,很值得学习。

expect(received).toMatchObject(expected);

jest 在对比 received 和 expected 值的时候,如果不匹配会把他们的 diff 显示出来。如果他们类型同为 map, array, object 类型之一的(为了便于理解此处省略较多步骤),会调用的 deepCyclicCopyReplaceable 进行深拷贝。

deepCyclicCopyReplaceable.ts

下面直接贴原码:

// 对象通过 value 参数传入
// *关键* 再用一个 cycles 来储存对象的子属性,防止循环引用。使用WeakMap 也能防止内存泄露
export default function deepCyclicCopyReplaceable<T>(
  value: T,
  cycles: WeakMap<any, any> = new WeakMap()
): T {
  if (typeof value !== "object" || value === null) {
    // 如果是 primitive 或者 function 直接返回该值
    return value;
  } else if (cycles.has(value)) {
    // *关键* 如果 cycles 里面有记录的值,直接返回记录的值
    // 走到这步说明有对象内有循环引用
    return cycles.get(value);
  } else if (Array.isArray(value)) {
    // 专门拷贝数组,具体实现不贴了
    // 简单说就是把 value 记录进 cycles,然后再创建一个同样长度的新数组,对数组内每一个元素递归调用 deepCyclicCopyReplaceable
    return deepCyclicCopyArray(value, cycles);
  } else if (isMap(value)) {
    // 专门拷贝 map
    // 原理同上,使用 Map.prototype.forEach 方法递归调用
    return deepCyclicCopyMap(value, cycles);
  } else if (isBuiltInObject(value)) {
    // 如果对象的构造函数是某些对象直接返回,如 Buffer,Date, Set, RegExp 等
    // 比如 const now = new Date();
    // now.constructor === Date
    return value;
  } else if (plugins.DOMElement.test(value)) {
    // 如果是 DOM 元素,使用 DOM 自带的 cloneNode 拷贝
    // plugins.DOMElement.test 具体实现略过
    // *关键* jest 是在 node 的环境执行的,但是他也会使用 jsdom 包来模拟浏览器里面的对象
    return (((value as unknown) as Element).cloneNode(true) as unknown) as T;
  } else {
    // 其他对象处理,见下方
    return deepCyclicCopyObject(value, cycles);
  }
}

function deepCyclicCopyArray<T>(
  array: Array<T>,
  cycles: WeakMap<any, unknown>
): T {
  // *关键* 用这个方法可以保证使用同一个原型
  const newObject = Object.create(Object.getPrototypeOf(object));
  const descriptors: {
    [x: string]: PropertyDescriptor;
  } = Object.getOwnPropertyDescriptors(object);
  // 记录引用,上一部分的 else if (cycles.has(value)) 会判断
  cycles.set(object, newObject);

  const newDescriptors = [
    ...Object.keys(descriptors),
    // *关键* 上一行 Object.keys 是获取不到 symbol 的
    // 需要用下一行的方法获取
    ...Object.getOwnPropertySymbols(descriptors),
  ];
  // 以下递归调用 deepCyclicCopyReplaceable 省略
}

总结

通过这个文件,几乎可以了解深拷贝的很多细节。比如复制 DOM 节点调用 cloneNode 方法。

但是这个方法并没有完整拷贝所有对象,他只是为了满足 jest-matcher-utils 包的功能。比如 Set 类型的值并没有拷贝,而是复用了原始对象的值。而且没有复制对象的 getters/setters,只复制了对应的 value。甚至抛弃了 WeekMap 里面的内容(因为对比不出来)。所以不能完全照搬,拿来参考借鉴还是不错的。

@NinoPanic
Copy link

跟金老师 学爪哇斯奎日噗!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants