Reactive Framework Core: Dependency Tracking and Propagation
Building reactive frameworks is a core concept in modern UI development (think React, Vue, Angular). This challenge focuses on creating the fundamental dependency tracking and propagation mechanism that underpins such frameworks. You'll build a system that allows values to "react" to changes in other values, automatically updating when dependencies change.
Problem Description
Your task is to implement a core reactive system in JavaScript. This system should allow you to define "signals" – values that can change over time. When a signal changes, any other signals that depend on it should automatically update. The system should handle multiple levels of dependencies and efficiently propagate changes only to the signals that are directly affected.
Key Requirements:
SignalClass: Create aSignalclass that represents a reactive value.- It should hold a value (initially provided during instantiation).
- It should maintain a list of
dependencies– otherSignalinstances that this signal depends on. - It should have a
valuegetter that returns the current value. - It should have a
set(newValue)method that updates the value and triggers updates to all dependent signals. - It should have an
addDependency(signal)method to register a dependency. - It should have a
removeDependency(signal)method to remove a dependency.
- Dependency Tracking: When a
Signal's value is set, thesetmethod should automatically notify all its dependencies that their values need to be recomputed. - Propagation: When a dependency's value changes, its dependent signals should be updated. This should be done efficiently, avoiding unnecessary updates.
- Circular Dependencies: The system should gracefully handle circular dependencies (e.g., Signal A depends on Signal B, and Signal B depends on Signal A). Avoid infinite loops. A simple approach is to prevent adding a signal as a dependency of itself.
- No direct DOM manipulation: This is a core framework component, so focus solely on the reactive logic.
Expected Behavior:
- When a signal's value is changed, all signals that directly or indirectly depend on it should be updated with the new value.
- Updates should propagate efficiently, only affecting signals that are actually dependent on the changed signal.
- Circular dependencies should be handled without causing errors or infinite loops.
Edge Cases to Consider:
- Signals depending on other signals that are themselves dependencies.
- Multiple signals depending on the same signal.
- Removing a dependency that is no longer needed.
- Circular dependencies.
- Setting a value to the same value it already holds.
Examples
Example 1:
// Create signals
const a = new Signal(1);
const b = new Signal(2);
const c = new Signal(3);
// Define dependencies
a.addDependency(b);
b.addDependency(c);
// Initial values
console.log(a.value); // Output: 1
console.log(b.value); // Output: 2
console.log(c.value); // Output: 3
// Change 'a'
a.set(5);
// Verify updates
console.log(a.value); // Output: 5
console.log(b.value); // Output: 5 (because b depends on a)
console.log(c.value); // Output: 3 (c is not affected)
Explanation: Changing a triggers an update in b because b depends on a. c is unaffected because it doesn't depend on a.
Example 2:
const x = new Signal(10);
const y = new Signal(20);
const z = new Signal(30);
x.addDependency(y);
y.addDependency(z);
z.addDependency(x); // Circular dependency
x.set(15);
console.log(x.value); // Output: 15
console.log(y.value); // Output: 20 (y is not directly affected by x)
console.log(z.value); // Output: 30 (z is not directly affected by x)
Explanation: The circular dependency is handled gracefully. The set method should not enter an infinite loop. The values remain unchanged because the dependency chain doesn't immediately trigger a recomputation.
Constraints
- Time Complexity: The
setmethod should have a time complexity of O(N), where N is the number of dependencies. While a more optimized solution is possible, this is a reasonable starting point. - Space Complexity: The space complexity should be reasonable for a typical number of signals (e.g., up to 1000).
- Input: Signals will be initialized with numerical values.
- Output: The
valuegetter should return the current numerical value of the signal. - No external libraries: You must implement the core logic yourself.
Notes
- Start by implementing the
Signalclass with the basicvalue,addDependency, andsetmethods. - Consider using a queue or other data structure to manage the propagation of updates.
- Think carefully about how to handle circular dependencies to prevent infinite loops. A simple check to prevent adding a signal as a dependency of itself is a good starting point.
- Focus on the core dependency tracking and propagation logic. UI rendering and other framework features are beyond the scope of this challenge.
- Test your implementation thoroughly with various scenarios, including nested dependencies and circular dependencies.