防止嵌套原型方法从 WebWorker 传输对象

Keeping nested prototype methods from object transfering from WebWorker

我需要从共享相同定义的 WebWorker 重新序列化一个对象。

收到消息后,我失去了所有原型功能。

正在做

worker.onmessage = ({ data }) => {
    this.programParser = Object.assign(new ProgramModel(), data);
}

仅适用于一级原型函数,但我需要一个适用于所有嵌套 classes 的解决方案(此对象具有大型继承和依赖树)。

我该怎么做才能做到这一点?

当前数据模型如下:

(注意 Object 原型)

使用扁平化和自定义 revive 函数(我在序列化函数的每个对象中添加了 class 名称),我可以获得更接近我需要的模型,但一些嵌套引用不是视为原始 class 个对象。

扁平化的恢复函数如下:

const model = parse(data, (key, val) => {
  if (val !== null && val.className && val.className in models) {
    if (val.className === "DataProvider") {
       console.log(val)
       return new DataProvider(val.providerData)
    }
    return Object.assign(new (<any>models)[val.className](), val)
   }
   return val;
 })

扁平化库用于清除 JSON 序列化的循环问题:https://github.com/WebReflection/flatted

问题的最小示例:https://codesandbox.io/s/zealous-https-n314w?file=/src/index.ts(二级引用后对象丢失)

这是循环结构上 tying the knot 的经典问题。反序列化时,flatted 库必须从某个地方开始,传入原始的尚未恢复的对象作为参数。理论上它可以传递一个代理(或一个带有 getter 的对象)以正确的顺序按需解析所涉及的对象,但是如果需要恢复圈子中的所有对象,这将导致堆栈溢出,因为 JS 不会不要使用惰性求值,您可以在求值之前参考调用结果。

一个纯粹的方法实际上需要一个支持这种惰性方法的 reviver 函数,直到反序列化完成后才访问传递给它的对象:

const cache = new WeakMap();
const ret = parse(str, (k, v) => {
  if (cache.has(v)) return cache.get(v);
  if (v && v.className && (<any>models)[v.className]) {
    const instance = new (<any>models)[v.className]();
    cache.set(v, instance);
    for (const p in instance) {
      Object.defineProperty(instance, p, {
        set(x) { v[p] = x; }, // not necessary if your models were immutable
        get() { return v[p]; },
        enumerable: true,
      });
    }
    return instance;
  }
  return v;
});

这实际上使 instance 成为 v 的代理,从那里获取其所有值。当 flatted 确实通过为 v 的属性分配恢复值来打结时,它们也将在 instance 上可用。除了 .className,在 reviver 调用期间没有访问 v 的属性。仅当您访问ret.something时,才会读取对象属性,届时将包含已恢复的对象。

这种方法的缺点是 a) 所有模型都需要预先声明和初始化它们的属性(例如您的 B 不会这样做)以便 for (const p in instance) 循环工作, 和 b) 你所有的模型属性都将被替换为访问器,这是低效的并且可能与内部实现冲突。

另一种方法是将 flatted 对原始对象所做的 属性 赋值转发给新构造的实例,即使它们发生在 reviver 调用之后:

const cache = new WeakMap();
const ret = parse(str, (k, v) => {
  if (cache.has(v)) return cache.get(v);
  if (v && v.className && (<any>models)[v.className]) {
    const instance = new (<any>models)[v.className]();
    cache.set(v, instance);
    Object.assign(instance, v);
    for (const p in v) {
      Object.defineProperty(v, p, {
        set(x) { instance[p] = x; },
        get() { return instance[p]; }, // not truly necessary but helps when `v` is logged
      });
    }
    return instance;
  }
  return v;
});

这基本上反映了您最初是如何使用 a.setB(new B(a)) 构造循环引用的,除了直接分配 属性 而不是使用模型的 setB 方法,reviver 不会这样做知道关于。使用这种方法时,请确保明确记录所有模型必须支持不带参数的构造函数调用,以及直接 属性 赋值(通过 Object.assigninstance[p] = x)。如果需要设置器,请使用访问器属性(set 语法)而不是 set…() 方法。

最简单且可能最快的方法是根本不 return 来自 reviver 的新对象,但保持其身份:

const ret = parse(str, (k, v) => {
  if (v && v.className) {
    const model = (<any>models)[v.className];
    if (model && Object.getPrototypeOf(v) != model.prototype) {
      Object.setPrototypeOf(v, model.prototype);
    }
  }
  return v;
});

这通过简单地将恢复对象的原型换成预期的原型来恢复原型方法。但是,这意味着您的模型是在没有调用构造函数的情况下实例化的,它们可能不使用可枚举的 getter,并且即使它们通过 .toJSON 正确公开它也无法维护私有状态。

我不想从针对此类图书馆的 , but while the explanation/process through is nice and sound, I believe the proposed solution is not the best one, and btw, I am the author of the flatted library that actually explained, and helped out, in this issue 中获得任何荣誉,甚至不知道这里有讨论......抱歉我来晚了......

前一个答案中遗漏的部分是同一个实例在恢复整个结构的同时被一遍又一遍地更新,但实际上不需要这样的东西,因为 SetWeakSet 都可以有助于加快流程,避免一遍又一遍地升级已经升级的内容。

const {setPrototypeOf} = Reflect;
const upgraded = new Set;
const ret = parse(str, (_, v) => {
  if (v && v.className && models[v.className] && !upgraded.has(v)) {
    upgraded.add(v);
    setPrototypeOf(v, models[v.className].prototype);
  }
  return v;
});

与替换相比,此更改并没有严格改进它起作用的原因或它是最佳解决方案,但它考虑了性能和冗余升级,因为不必要的 setPrototypeOf 调用,可能不会完全需要