Implementing Vue's Reactive Effect Tracking System
Vue's reactivity system is a cornerstone of its efficiency. It relies on a mechanism to track which "effects" (like component rendering functions or computed properties) depend on which reactive data. This challenge asks you to build a simplified version of this effect tracking system to understand its core principles.
Problem Description
Your task is to create a system that allows you to:
- Define reactive properties: Create objects whose property changes can be observed.
- Create effects: Define functions that should re-run whenever a reactive property they depend on changes.
- Track dependencies: When an effect runs, record which reactive properties it accessed.
- Trigger re-runs: When a reactive property changes, re-run all the effects that depend on it.
You will need to implement two main functions: effect and track (and trigger implicitly).
Key Requirements:
effect(fn): This function takes a callback functionfnas an argument. It should immediately executefnand then set up a mechanism to re-executefnwhenever any of the reactive dependencies accessed withinfnchange.track(target, key): This function will be called inside an effect's execution when a reactive property is accessed. It needs to record that the current active effect depends ontarget[key].- Implicit
trigger(target, key): While not a separate function to implement, your system should internally handle the logic to re-run effects when a property is set. This will be triggered by setting a new value on a reactive object.
Expected Behavior:
When effect is called with a function, that function should execute. If, during its execution, it accesses a reactive property (e.g., state.count), the track function should be called to record this dependency. When the value of that reactive property is later changed, the previously registered effect should be automatically re-executed.
Edge Cases:
- An effect might access a property multiple times; it should only be tracked once.
- An effect might access properties that are not reactive; these should be ignored.
- Consider scenarios where an effect might be re-run while it's already running (e.g., an effect modifying a property it depends on). For simplicity in this challenge, assume such recursive triggers are not a primary concern, but your tracking should ideally prevent infinite loops.
Examples
Example 1:
// Assume reactive, track, and trigger are implemented
const state = { count: 0 };
const effect1 = effect(() => {
console.log(`Count is: ${state.count}`);
});
// Initial run of effect1:
// Output: Count is: 0
state.count++;
// After changing state.count:
// Output: Count is: 1
Explanation:
effect1is created with a function that logsstate.count.- The
effectfunction executes() => { console.log(...) }. - Inside the effect,
state.countis accessed. This triggerstrack(state, 'count'). - The dependency is recorded: effect1 depends on
state.count. state.countis incremented. This triggers an internaltriggerforstateandcount.- The
triggerfinds thateffect1depends onstate.countand re-runseffect1.
Example 2:
const state = { count: 0, message: "hello" };
const computedCount = { value: 0 };
const computedMessage = { value: "" };
const effectCount = effect(() => {
computedCount.value = state.count * 2;
console.log(`Computed count: ${computedCount.value}`);
});
const effectMessage = effect(() => {
computedMessage.value = state.message.toUpperCase();
console.log(`Computed message: ${computedMessage.value}`);
});
// Initial runs:
// Output: Computed count: 0
// Output: Computed message: HELLO
state.count = 5;
// Output: Computed count: 10
state.message = "world";
// Output: Computed message: WORLD
Explanation:
effectCountdepends onstate.count. Whenstate.countchanges,effectCountre-runs and updatescomputedCount.value.effectMessagedepends onstate.message. Whenstate.messagechanges,effectMessagere-runs and updatescomputedMessage.value.- Changing one property (
state.count) does not re-run theeffectMessagebecause it doesn't depend onstate.count.
Example 3: Re-running an effect that modifies dependencies
const state = { count: 0 };
const effectRecursive = effect(() => {
console.log(`Current count in effect: ${state.count}`);
if (state.count < 3) {
state.count++; // This will trigger a re-run
}
});
// Initial run and subsequent re-runs due to state.count++
// Output: Current count in effect: 0
// Output: Current count in effect: 1
// Output: Current count in effect: 2
// Output: Current count in effect: 3
Explanation:
effectRecursiveruns, logs0.state.countbecomes1. This triggers a re-run.effectRecursiveruns again, logs1.state.countbecomes2. This triggers a re-run.effectRecursiveruns again, logs2.state.countbecomes3. This triggers a re-run.effectRecursiveruns again, logs3.- The condition
state.count < 3is now false, sostate.countis not incremented, and the effect stops re-running.
Constraints
- The
trackfunction will be called within an activeeffect. - You will need to manage a global or shared state to keep track of the currently active effect.
- You will need a data structure to store dependencies (e.g., mapping reactive objects and keys to sets of effects).
- The solution should be implemented in TypeScript.
Notes
- This challenge focuses on the core mechanism of dependency tracking and effect re-execution. It's a simplified model of Vue's reactivity.
- You'll likely need a way to store the "current effect" that is running. A global variable or a stack can be useful here.
- Think about how to associate effects with specific properties of reactive objects. A
WeakMapmapping target objects toMaps of keys to sets of effects is a common pattern. - The "reactive" nature of an object can be simulated by using
Proxyor by manually wrapping property accessors. For this challenge, you can assumestateobjects are either already proxied or you will be provided with functions to make them reactive. Your primary focus is theeffectandtrack/triggerlogic. - Consider how to avoid infinite loops when an effect modifies its own dependencies. For this challenge, simply ensuring the effect eventually stops running is sufficient.