如何跟踪 Javascript 中共享的不可变引用类型的变化

How to track changes with shared, immutable reference types in Javascript

考虑以下示例:

function add(x, y) { return x + y; }

var collection = Object.freeze([1, 2, 3, 4]);
var consumerA = collection; // expects steady data
var consumerB = collection; // requires the latest data
var updatedCollection = collection.concat(5);

consumerA.reduce(add, 0); // 10 (desired result)
consumerB.reduce(add, 0); // 10 (incorrect result, should be 15)

consumerA 使用它期望的不可变数据进行操作。在 Javascript 中可以做什么来确保 consumerB 始终访问最新数据?

请注意:仅深度复制 consumerA 并将 collection 视为可变数据不是一种选择。

更新:该示例仅用于说明由共享引用类型引起的基本问题:一些消费者(或引用持有者)依赖不可变数据,其他消费者依赖可变数据.我正在寻找一种适当的更改跟踪机制来解决这个问题,同时又不会破坏不可变数据的好处。

也许 "change tracking" 这个词太含糊了。对于更改跟踪,我指的是一种让 consumerB 了解更改(推送机制)或(更有趣)能够发现更改(拉动机制)的方法。后者需要 consumerB 以某种方式访问​​更新的集合。

声明 collection 时使用 Object.freeze,因此无法向 collection 添加属性。

当您创建 consumerB 时,您执行对象的副本 collection

var consumerB = collection; 

因此您不能向 consumerB 添加属性,例如 collection

您需要克隆对象而不是复制它。你可以这样做:

var consumerB = JSON.parse(JSON.stringify(collection)); 

好吧,这是我唯一的解决方案,但可能还有其他解决方案。我将我的不可变集合包装在一个可变对象中。需要常量数据的消费者持有对集合本身的引用。需要当前状态的消费者持有对包装器的引用。我使用原始形式的结构共享以避免克隆:

function add(x, y) { return x + y; }

var collection = Object.freeze([1, 2, 3, 4]);
var atom = {state: collection};
var consumerA = collection;
var consumerB = atom;

console.log(consumerA === consumerB.state); // true (obviously)

// naive structural sharing to avoid cloning
atom.state = Object.create(atom.state, {length: {value: atom.state.length, writable: true}});
atom.state.push(5);
Object.freeze(atom.state);

// as desired
console.log(consumerA.reduce(add, 0)); // 10
console.log(consumerB.state.reduce(add, 0)); // 15

// structural sharing is used
console.log(Object.getPrototypeOf(consumerB.state) === collection); // true

// object comparison simply by reference check
console.log(consumerA === consumerB.state); // false

通过将不可变集合包装在可变包装器中,它成为一种持久数据类型。这意味着它可以被视为一个普通的、可变的对象,但保持其以前的版本不变,因此是持久的。顺便说一句,将包装器命名为 atom 并非偶然,而是对 Clojure 中相应数据类型的引用。

请注意:使用原型系统进行结构共享会导致内存泄漏,请谨慎使用。