Hone logo
Hone
Problems

Implement Deep Watcher in Vue.js

Vue.js provides a powerful watch option to react to changes in your component's data. However, the default watch behavior only detects changes at the top level of an object. This challenge asks you to implement a "deep watcher" that can detect modifications within nested properties of an object, similar to Vue's deep: true option. This is crucial for complex data structures where you need to be notified even if a deeply nested property changes.

Problem Description

Your task is to create a custom Vue directive or a reusable composable function (if using Vue 3 Composition API) that allows you to "deep watch" a reactive object. This deep watcher should trigger a callback function whenever any property within the object, at any level of nesting, is modified.

Key Requirements:

  • Recursive Observation: The watcher must recursively observe all properties of the target object, including nested objects and arrays.
  • Callback Execution: When a change is detected in any nested property, a provided callback function must be executed.
  • Change Detection: The callback should receive the new value and the old value of the changed property.
  • Unwatch Functionality: The watcher should expose a method to stop watching and clean up any listeners.

Expected Behavior:

When a deep watcher is set up on an object, and a property within that object (or within a nested object or array) is modified, the registered callback function should be invoked with the new and old values of that specific property.

Edge Cases to Consider:

  • Adding/Deleting Properties: How should the watcher handle the addition or deletion of properties within an object? (For this challenge, assume we only need to watch for modifications to existing properties and elements within arrays. Adding/deleting properties is out of scope unless explicitly stated).
  • Circular References: While unlikely in typical Vue data, consider how to prevent infinite loops if the data structure contains circular references.
  • Initial Value: The callback should not be triggered by the initial setup of the watcher, only by subsequent changes.
  • Array Modifications: Ensure that changes to array elements (e.g., arr[index] = newValue) and array mutations (e.g., push, pop, splice) are detected.

Examples

Example 1:

// Assume 'data' is a reactive object in a Vue component
const data = reactive({
  user: {
    name: 'Alice',
    address: {
      street: '123 Main St',
      city: 'Anytown'
    }
  },
  settings: {
    theme: 'dark'
  }
});

// Setup a deep watcher on 'data'
const stopWatching = deepWatch(data, (newValue, oldValue, path) => {
  console.log(`Property changed at path "${path}":`);
  console.log('Old Value:', oldValue);
  console.log('New Value:', newValue);
});

// Trigger changes:
data.user.name = 'Bob';
// Expected Output:
// Property changed at path "user.name":
// Old Value: Alice
// New Value: Bob

data.user.address.city = 'Otherville';
// Expected Output:
// Property changed at path "user.address.city":
// Old Value: Anytown
// New Value: Otherville

data.settings.theme = 'light';
// Expected Output:
// Property changed at path "settings.theme":
// Old Value: dark
// New Value: light

// Later, to stop watching:
// stopWatching();

Example 2:

// Assume 'list' is a reactive array
const list = reactive([
  { id: 1, value: 'A' },
  { id: 2, value: 'B' }
]);

// Setup a deep watcher on 'list'
const stopWatching = deepWatch(list, (newValue, oldValue, path) => {
  console.log(`List item changed at path "${path}":`);
  console.log('Old Value:', oldValue);
  console.log('New Value:', newValue);
});

// Trigger changes:
list[0].value = 'X';
// Expected Output:
// List item changed at path "0.value":
// Old Value: A
// New Value: X

list.push({ id: 3, value: 'C' }); // Note: Adding/deleting elements is out of scope for this specific callback
// For this challenge, assume we only care about modification of existing elements.

// Example of how a modification to a *new* element might be handled if the watcher was re-applied or intelligently updated (advanced).
// For the core problem, focus on existing elements.

// To stop watching:
// stopWatching();

Constraints

  • The target object for watching will be a standard JavaScript object or array.
  • The nested properties will also be objects or arrays.
  • The callback function will be provided and must accept newValue, oldValue, and path (string representing the dot-notation path to the changed property).
  • Performance: The implementation should be reasonably efficient, avoiding excessive overhead for simple data structures while still being capable of handling moderately nested objects. Avoid infinite recursion for circular references.

Notes

  • This challenge is about replicating the functionality of Vue's deep: true option.
  • Consider using Vue's reactivity system (e.g., reactive, watchEffect or watch if building a composable) as a foundation.
  • You will likely need a recursive approach to traverse nested structures.
  • Think about how to keep track of the path to the currently observed property to pass to the callback.
  • For the stopWatching function, ensure all internal listeners are properly cleaned up to prevent memory leaks.
  • The path argument in the callback is a helpful addition for debugging and understanding where the change occurred.
Loading editor...
typescript