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:
- Property Tracking: The watcher should be able to track changes to a specific property of the reactive object.
- Callback Execution: When the tracked property's value changes, the provided callback function should be executed.
- Initial Value: The callback should not be executed on the initial setup if the
immediateoption is not provided or isfalse. immediateOption: The returned watcher function should accept an optional options object, including animmediateboolean property. Ifimmediateistrue, the callback should be executed once immediately upon setting up the watcher, even if the value hasn't changed yet.- 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
unwatchfunction, 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
createWatcherfunction must be written in TypeScript. - The reactive object passed to
createWatcherwill 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 usingObject.definePropertyor a proxy for simplified simulation). - The callback function should accept
newValueandoldValueas arguments. - The
immediateoption 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
watchAPI 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.definePropertyto 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). YourcreateWatcherfunction 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
createWatcherclosure to manage multiple watchers on the same object. - To implement deep watching, you'd need to recursively observe nested objects and arrays.