Question 59

Question

How would you implement a custom observable pattern using proxies and Symbol.iterator?

Answer

Here's an implementation demonstrating how to achieve this:

const observableSymbols = {
  observers: Symbol('observers'), // Store observers within the proxy
};

function createObservable(target) {
  // Create a Proxy with observer management
  return new Proxy(target, {
    get(target, prop, receiver) {
      // Get property value as usual
      return Reflect.get(target, prop, receiver); 
    },
    set(target, prop, newValue, receiver) {
      // Set the property value
      Reflect.set(target, prop, newValue, receiver);

      // Notify observers about the change
      for (const observer of target[observableSymbols.observers]) {
        observer(prop, newValue, target); // Call observer function with updated data
      }
      return true;
    },
  });
}

// Custom `Symbol.iterator` for observable values
function* observableIterator(target) {
  for (const key in target) {
    yield [key, target[key]]; 
  }
}

// Example Usage:

const myObservable = createObservable({ name: 'Alice', age: 30 });
myObservable[observableSymbols.observers] = []; // Initialize observer list for the proxy
myObservable.age = 35;


// Observer function (registers to listen to changes)
function logChange(prop, newValue, target) {
  console.log(`Property ${prop} changed to: ${newValue}`);
}

const observer = logChange;
myObservable[observableSymbols.observers].push(observer); // Register the observer

Explanation:

  1. createObservable() Function:

    • It takes a target object and creates a new Proxy around it.

    • The proxy's set() handler updates the property value and then iterates through the registered observers, calling each observer function with the changed property name, new value, and the original target object.

  2. observableSymbols: This object uses Symbols to store internal properties within our proxy (like observers) to avoid collisions with potential existing properties on the target object.

  3. Symbol.iterator: The observableIterator function provides an iterator-based way to loop through the key-value pairs of the observable object, enabling you to consume its values.

  4. Observer Registration: The myObservable[observableSymbols.observers] array holds any functions that want to be notified about changes in the observable object.

Key Advantages:

  • Customizability: You can tailor the behavior of observers by defining their function signatures and the data they receive.

  • Clean Separation: Observers are clearly separated from the observable object itself, promoting modularity.

Last updated