Hone logo
Hone
Problems

Implementing a Custom Vue Watcher

This challenge focuses on understanding and implementing the core concept of Vue's reactivity system, specifically how watchers track changes and trigger side effects. You will create a custom "watch" function that mimics Vue's behavior, allowing you to monitor specific data properties and execute a callback function whenever those properties change. This is a fundamental building block for creating dynamic and responsive user interfaces.

Problem Description

Your task is to implement a function createWatcher that takes a reactive object and a callback function as arguments. This createWatcher function should return another function that, when called with a property name, will set up a watcher for that property on the reactive object.

Key Requirements:

  1. Property Tracking: The watcher should be able to track changes to a specific property of the reactive object.
  2. Callback Execution: When the tracked property's value changes, the provided callback function should be executed.
  3. Initial Value: The callback should not be executed on the initial setup if the immediate option is not provided or is false.
  4. immediate Option: The returned watcher function should accept an optional options object, including an immediate boolean property. If immediate is true, the callback should be executed once immediately upon setting up the watcher, even if the value hasn't changed yet.
  5. Deep Watching (Optional but Recommended): For object or array properties, consider how you might handle deep watching. If the property is an object or array, changes within its nested properties should also trigger the watcher. (For this challenge, you can simplify and assume shallow watching or focus on primitive types if deep watching is too complex for your current scope.)

Expected Behavior:

When you set up a watcher for a property using the returned function, and that property's value is later modified, the callback function will be invoked with the new value and potentially the old value.

Edge Cases to Consider:

  • Property Not Found: What happens if you try to watch a property that doesn't exist on the reactive object?
  • Non-Primitive Values: How do you handle watching properties that are objects or arrays?
  • Unwatching (Implicit): While not explicitly required to implement an unwatch function, consider the lifecycle of a watcher. In a real Vue application, you'd need a way to clean up watchers. For this exercise, focus on the core watching mechanism.

Examples

Example 1: Basic Watcher

// Assume 'reactiveData' is a simple object with primitive properties
const reactiveData = {
  count: 0,
  message: 'Hello'
};

// This is a simplified representation of reactivity for the challenge
// In a real Vue app, this would be managed by Vue's internal system.
function makeReactive<T extends object>(obj: T): T {
  const handlers = {
    set(target: any, property: string, value: any) {
      target[property] = value;
      // In a real scenario, this would trigger notifications to watchers
      return true;
    }
  };
  return new Proxy(obj, handlers);
}

const observedData = makeReactive(reactiveData);

const watcherCallback = (newValue: any, oldValue: any) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
};

const createWatcher = (obj: Record<string, any>, callback: (newValue: any, oldValue: any) => void) => {
  const watchers: Record<string, { callback: typeof callback, options?: { immediate?: boolean } }> = {};

  const watch = (property: string, options?: { immediate?: boolean }) => {
    watchers[property] = { callback, options };

    // Simplified notification mechanism for demonstration
    const originalSet = obj[property]; // Store initial value
    Object.defineProperty(obj, property, {
      get() {
        return originalSet;
      },
      set(newValue) {
        const oldValue = originalSet;
        originalSet = newValue;
        // Simulate Vue's reactivity trigger
        if (watchers[property] && watchers[property].callback) {
          watchers[property].callback(newValue, oldValue);
        }
      }
    });

    if (options?.immediate) {
      callback(obj[property], undefined); // Call immediately with current value and no old value
    }
  };
  return watch;
};

const watchCount = createWatcher(observedData, watcherCallback);

// Setup watcher
watchCount('count');

// Trigger change
observedData.count = 5; // Output: Count changed from 0 to 5
observedData.count = 10; // Output: Count changed from 5 to 10

Example 2: Watcher with immediate: true

const reactiveData = {
  user: { name: 'Alice' }
};
const observedData = makeReactive(reactiveData); // Using makeReactive from Example 1

const userNameWatcher = (newValue: any, oldValue: any) => {
  console.log(`User name is now: ${newValue}`);
};

const createWatcher = (obj: Record<string, any>, callback: (newValue: any, oldValue: any) => void) => {
  const watchers: Record<string, { callback: typeof callback, options?: { immediate?: boolean } }> = {};

  const watch = (property: string, options?: { immediate?: boolean }) => {
    watchers[property] = { callback, options };

    // Simplified notification mechanism for demonstration
    const originalSet = obj[property];
    Object.defineProperty(obj, property, {
      get() {
        return originalSet;
      },
      set(newValue) {
        const oldValue = originalSet;
        originalSet = newValue;
        if (watchers[property] && watchers[property].callback) {
          watchers[property].callback(newValue, oldValue);
        }
      }
    });

    if (options?.immediate) {
      callback(obj[property], undefined);
    }
  };
  return watch;
};

const watchUserName = createWatcher(observedData, userNameWatcher);

// Setup watcher with immediate execution
watchUserName('user.name', { immediate: true }); // Output: User name is now: Alice (immediately)

// Trigger change
observedData.user.name = 'Bob'; // Output: User name is now: Bob

Example 3: Watching a non-existent property

const reactiveData = {
  value: 100
};
const observedData = makeReactive(reactiveData);

const logChange = (newValue: any, oldValue: any) => {
  console.log(`Value changed: ${oldValue} -> ${newValue}`);
};

const createWatcher = (obj: Record<string, any>, callback: (newValue: any, oldValue: any) => void) => {
  // ... (implementation from previous examples) ...
  const watchers: Record<string, { callback: typeof callback, options?: { immediate?: boolean } }> = {};

  const watch = (property: string, options?: { immediate?: boolean }) => {
    // Simplified check for property existence
    if (!(property in obj)) {
      console.warn(`Property "${property}" does not exist on the observed object.`);
      return;
    }

    watchers[property] = { callback, options };

    const originalSet = obj[property];
    Object.defineProperty(obj, property, {
      get() {
        return originalSet;
      },
      set(newValue) {
        const oldValue = originalSet;
        originalSet = newValue;
        if (watchers[property] && watchers[property].callback) {
          watchers[property].callback(newValue, oldValue);
        }
      }
    });

    if (options?.immediate) {
      callback(obj[property], undefined);
    }
  };
  return watch;
};

const watchNonExistent = createWatcher(observedData, logChange);

watchNonExistent('nonExistentProperty'); // Output: Property "nonExistentProperty" does not exist on the observed object.

Constraints

  • The createWatcher function must be written in TypeScript.
  • The reactive object passed to createWatcher will be a plain JavaScript object. You can assume it's already "reactive" in the sense that we can hook into its property setters (as demonstrated in the examples using Object.defineProperty or a proxy for simplified simulation).
  • The callback function should accept newValue and oldValue as arguments.
  • The immediate option should be handled correctly.
  • Focus on watching top-level properties for simplicity. Deep watching is a stretch goal.

Notes

  • This challenge is designed to simulate Vue's watch API at a conceptual level. You won't be working with Vue's actual reactivity system directly.
  • Consider how you will store the watcher configurations and trigger the callbacks.
  • The examples use Object.defineProperty to simulate reactivity for demonstration. In a real Vue application, this would be handled by Vue's internal reactivity engine (e.g., Proxies in Vue 3). Your createWatcher function should operate on an object that behaves as if it has this kind of reactivity.
  • Think about the state that needs to be maintained within the createWatcher closure to manage multiple watchers on the same object.
  • To implement deep watching, you'd need to recursively observe nested objects and arrays.
Loading editor...
typescript