Hone logo
Hone
Problems

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:

  1. Define reactive properties: Create objects whose property changes can be observed.
  2. Create effects: Define functions that should re-run whenever a reactive property they depend on changes.
  3. Track dependencies: When an effect runs, record which reactive properties it accessed.
  4. 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 function fn as an argument. It should immediately execute fn and then set up a mechanism to re-execute fn whenever any of the reactive dependencies accessed within fn change.
  • 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 on target[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:

  1. effect1 is created with a function that logs state.count.
  2. The effect function executes () => { console.log(...) }.
  3. Inside the effect, state.count is accessed. This triggers track(state, 'count').
  4. The dependency is recorded: effect1 depends on state.count.
  5. state.count is incremented. This triggers an internal trigger for state and count.
  6. The trigger finds that effect1 depends on state.count and re-runs effect1.

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:

  • effectCount depends on state.count. When state.count changes, effectCount re-runs and updates computedCount.value.
  • effectMessage depends on state.message. When state.message changes, effectMessage re-runs and updates computedMessage.value.
  • Changing one property (state.count) does not re-run the effectMessage because it doesn't depend on state.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:

  1. effectRecursive runs, logs 0.
  2. state.count becomes 1. This triggers a re-run.
  3. effectRecursive runs again, logs 1.
  4. state.count becomes 2. This triggers a re-run.
  5. effectRecursive runs again, logs 2.
  6. state.count becomes 3. This triggers a re-run.
  7. effectRecursive runs again, logs 3.
  8. The condition state.count < 3 is now false, so state.count is not incremented, and the effect stops re-running.

Constraints

  • The track function will be called within an active effect.
  • 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 WeakMap mapping target objects to Maps of keys to sets of effects is a common pattern.
  • The "reactive" nature of an object can be simulated by using Proxy or by manually wrapping property accessors. For this challenge, you can assume state objects are either already proxied or you will be provided with functions to make them reactive. Your primary focus is the effect and track/trigger logic.
  • 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.
Loading editor...
typescript