Building a Vue-Inspired Reactivity System in TypeScript
Vue.js is renowned for its elegant and efficient reactivity system, which automatically updates the UI when the underlying data changes. This challenge aims to replicate the core functionality of such a system using TypeScript, allowing you to gain a deeper understanding of how this powerful feature works under the hood.
Problem Description
Your task is to create a simplified reactivity system in TypeScript that mimics the fundamental behavior of Vue's reactivity. This system should allow you to define reactive data properties and automatically trigger callbacks (effects) whenever these properties change.
Key Requirements:
reactivefunction: Create a functionreactive(obj)that takes a plain JavaScript object and returns a reactive proxy of that object.trackfunction: Implement a mechanism to track which effects are reading which properties.triggerfunction: Implement a mechanism to trigger effects when a property is written to.effectfunction: Create a functioneffect(callback)that takes a callback function. This callback should be executed immediately, and any reactive properties accessed within it should be "tracked." The callback should then be re-executed whenever any of the tracked properties change.- Handling nested objects: The
reactivefunction should recursively make nested objects reactive. - Handling array modifications: The system should also support reactivity for array operations (e.g.,
push,pop,splice).
Expected Behavior:
When a reactive property is accessed within an effect's callback, that access should be recorded. When that same reactive property is later modified, the effect's callback should be re-executed.
Edge Cases:
- Modifying a property to its current value should not trigger an effect re-execution.
- Accessing properties that don't exist on the initial object should be handled gracefully (though for this challenge, assume initial properties exist or are added later).
- Ensure that nested objects within the reactive object are also reactive.
Examples
Example 1: Basic Property Update
// Assume reactive, track, trigger, effect are defined
const state = reactive({ count: 0 });
effect(() => {
console.log(`Count is: ${state.count}`);
});
state.count++; // This should trigger the effect, logging "Count is: 1"
state.count++; // This should trigger the effect again, logging "Count is: 2"
Output:
Count is: 0
Count is: 1
Count is: 2
Explanation:
The initial effect runs, logging Count is: 0. When state.count is incremented, the trigger mechanism detects the change and re-executes the effect's callback, logging the new value.
Example 2: Nested Object and Multiple Properties
// Assume reactive, track, trigger, effect are defined
const user = reactive({
name: "Alice",
address: {
city: "Wonderland",
zip: "12345"
}
});
effect(() => {
console.log(`User: ${user.name}, City: ${user.address.city}`);
});
user.name = "Bob"; // Triggers effect
user.address.city = "Oz"; // Triggers effect
user.address.zip = "67890"; // Does NOT trigger effect (zip is not tracked in the current effect)
Output:
User: Alice, City: Wonderland
User: Bob, City: Wonderland
User: Bob, City: Oz
Explanation:
The initial effect logs "User: Alice, City: Wonderland". When user.name changes, the effect re-runs. When user.address.city changes, the effect re-runs again. user.address.zip changes, but since it wasn't accessed in the effect's callback, no re-execution occurs.
Example 3: Array Modification
// Assume reactive, track, trigger, effect are defined
const list = reactive({ items: ["a", "b"] });
effect(() => {
console.log(`Items: ${list.items.join(", ")} (length: ${list.items.length})`);
});
list.items.push("c"); // Triggers effect
list.items.pop(); // Triggers effect
Output:
Items: a, b (length: 2)
Items: a, b, c (length: 3)
Items: a, b (length: 2)
Explanation:
The initial effect logs the starting list. Pushing "c" makes the list reactive, triggering the effect. Popping "c" again makes the list reactive, triggering the effect once more.
Constraints
- The
reactive,track,trigger, andeffectfunctions must be implemented in TypeScript. - The system should handle primitive types (strings, numbers, booleans) and nested objects for reactivity.
- Array modifications like
push,pop,splice,lengthchanges should be supported. - The performance should be reasonably efficient for typical use cases, avoiding unnecessary re-executions of effects.
Notes
- You will likely need to use
Proxyobjects to intercept property access and modification. - Consider how to store and manage the dependencies between effects and reactive properties. A common pattern involves using a
Mapto store dependencies. - Think about how to handle the initial run of an
effectand subsequent updates. - For array reactivity, you might need to intercept specific array methods.