Minimal Reactive System in TypeScript
This challenge asks you to implement a simplified reactivity system, mimicking core aspects of Vue's reactivity. Building such a system helps understand how frameworks track dependencies and trigger updates when data changes. This exercise focuses on basic dependency tracking and notification, laying the groundwork for more complex reactivity features.
Problem Description
You need to create a Reactive class that can make plain JavaScript objects reactive. When a property of a reactive object is accessed, it should be tracked as a dependency. When that property is modified, all dependent functions (those that accessed the property) should be notified and re-executed.
Key Requirements:
Reactive(obj: object): A function that takes a plain JavaScript object and returns a reactive version of it.- Dependency Tracking: When a property of the reactive object is accessed within a function, that function should be registered as a dependency of that property.
- Change Notification: When a property of the reactive object is modified, all functions registered as dependencies on that property should be called.
track(target: Function, key: string): A helper function to register a dependency.trigger(target: object, key: string): A helper function to notify dependencies.
Expected Behavior:
- Creating a reactive object should not modify the original object.
- Accessing a property of a reactive object should register the current function as a dependency.
- Modifying a property of a reactive object should trigger all registered dependencies.
- Dependencies should be tracked correctly even with nested properties.
Edge Cases to Consider:
- Objects with circular references (avoid infinite loops). While a full circular reference detection isn't required, your code shouldn't crash if one is encountered.
- Modifying properties that are not reactive (e.g., adding a new property).
- Multiple accesses to the same property within different functions.
- Nested reactive objects.
Examples
Example 1:
Input:
const data = { count: 0 };
const reactiveData = Reactive(data);
function updateCount() {
console.log("Count updated:", reactiveData.count);
}
reactiveData.count = 1;
Output:
Count updated: 1
Explanation: updateCount is registered as a dependency of reactiveData.count. When reactiveData.count is set to 1, updateCount is called.
Example 2:
Input:
const data = { user: { name: "Alice" } };
const reactiveData = Reactive(data);
function updateName() {
console.log("Name updated:", reactiveData.user.name);
}
reactiveData.user.name = "Bob";
Output:
Name updated: Bob
Explanation: updateName is registered as a dependency of reactiveData.user.name. When reactiveData.user.name is set to "Bob", updateName is called.
Example 3: (Multiple accesses)
Input:
const data = { message: "Hello" };
const reactiveData = Reactive(data);
function displayMessage1() {
console.log("Message 1:", reactiveData.message);
}
function displayMessage2() {
console.log("Message 2:", reactiveData.message);
}
displayMessage1();
displayMessage2();
reactiveData.message = "World";
Output:
Message 1: Hello
Message 2: Hello
Message 1: World
Message 2: World
Explanation: Both displayMessage1 and displayMessage2 are registered as dependencies of reactiveData.message. When reactiveData.message is updated, both functions are called.
Constraints
- The solution must be written in TypeScript.
- The
Reactivefunction should handle objects with a depth of at least 2 levels of nesting. - The solution should avoid infinite loops when encountering circular references (though full circular reference detection is not required).
- Performance is not a primary concern for this exercise, but avoid unnecessarily complex or inefficient code.
Notes
- You'll need to use
WeakMapto store dependency information without preventing garbage collection. - Consider using
Proxyfor property access and modification interception. IfProxyis not available (e.g., older browsers), you can useObject.definePropertybut this is significantly more complex. For this challenge, usingProxyis strongly recommended. - Think about how to handle the
thiscontext within the dependent functions. The current implementation doesn't need to preserve thethiscontext, but it's a consideration for a more complete reactivity system. - Focus on the core dependency tracking and notification logic. Error handling and more advanced features (e.g., computed properties, watchers) are beyond the scope of this challenge.