Hone logo
Hone
Problems

Implementing Custom Reactivity in Vue with TypeScript

Vue.js's reactivity system is a core feature, automatically tracking dependencies and updating the DOM when data changes. This challenge asks you to build a simplified, custom reactivity system from scratch, mimicking Vue's core behavior. This exercise will deepen your understanding of how reactivity works under the hood and provide valuable insights into Vue's internals.

Problem Description

You are tasked with creating a basic reactivity system in TypeScript. This system should allow you to:

  1. Make data reactive: A function that takes a plain JavaScript object and returns a reactive version of it. Changes to properties within this reactive object should trigger updates to dependent functions.
  2. Create reactive dependencies: A function that takes a reactive object and a callback function. This callback function should be executed whenever any property of the reactive object changes.
  3. Update reactive data: A function to modify a property of a reactive object. This function must trigger all registered dependencies.

The core of the system revolves around tracking which functions (dependencies) are watching which properties of the reactive object. When a property changes, all functions that depend on that property should be re-executed.

Key Requirements:

  • The system should handle nested objects correctly. Changes to properties within nested objects should also trigger dependencies.
  • The system should avoid infinite loops when updating reactive data.
  • The system should be relatively performant – avoid unnecessary re-executions of dependencies.
  • The system should be written in TypeScript, leveraging its type safety features.

Expected Behavior:

  • When a reactive object is created, its properties are initially tracked.
  • When a dependency is registered, it's associated with the specific property it depends on.
  • When a property is updated, all registered dependencies for that property are executed.
  • Nested properties should also trigger dependencies.

Edge Cases to Consider:

  • Modifying the reactive object itself (e.g., reactiveObject = { ... }). This should not trigger dependencies. Only property modifications should.
  • Dependencies on the same property from different functions. Both functions should be executed.
  • Dependencies on properties that don't exist. Handle this gracefully (e.g., by not registering the dependency).
  • Deleting properties from the reactive object. This should trigger dependencies if any functions depend on that property.

Examples

Example 1:

Input:
const data = { a: 1, b: { c: 2 } };
const reactiveData = makeReactive(data);

const updateAndCheck = () => {
  console.log("Update and Check called");
};

addDependency(reactiveData, updateAndCheck, 'a');
addDependency(reactiveData, updateAndCheck, 'b.c');

updateReactive(reactiveData, 'a', 2);

Output:

Update and Check called

Explanation: makeReactive creates a reactive version of data. addDependency registers updateAndCheck as a dependency on properties 'a' and 'b.c'. updateReactive modifies 'a' to 2, triggering updateAndCheck.

Example 2:

Input:
const data = { x: 10 };
const reactiveData = makeReactive(data);

const dependency1 = () => { console.log("Dependency 1"); };
const dependency2 = () => { console.log("Dependency 2"); };

addDependency(reactiveData, dependency1, 'x');
addDependency(reactiveData, dependency2, 'x');

updateReactive(reactiveData, 'x', 20);

Output:

Dependency 1
Dependency 2

Explanation: Both dependency1 and dependency2 depend on 'x'. Updating 'x' triggers both dependencies.

Example 3: (Edge Case - Deletion)

Input:
const data = { d: 4 };
const reactiveData = makeReactive(data);

const dependency = () => { console.log("Dependency on d"); };

addDependency(reactiveData, dependency, 'd');

updateReactive(reactiveData, 'd', undefined); // Simulate deletion

Output:

Dependency on d

Explanation: Deleting the property 'd' should still trigger any dependencies on 'd'.

Constraints

  • Time Complexity: makeReactive, addDependency, and updateReactive should ideally have a time complexity of O(1) or O(n) where n is a small constant related to dependency tracking. Avoid O(n) where n is the size of the reactive object.
  • Space Complexity: The space complexity should be reasonable, avoiding excessive memory usage.
  • Input Type: The makeReactive function should accept a plain JavaScript object. addDependency should accept the reactive object, a callback function, and a string representing the property path (e.g., 'a' or 'b.c'). updateReactive should accept the reactive object, the property path, and the new value.
  • No External Libraries: You cannot use any external libraries (e.g., Vue.js itself, Lodash). This is about implementing the core reactivity logic.

Notes

  • Consider using a Map or WeakMap to store dependencies.
  • Think about how to handle property paths (e.g., 'b.c') efficiently. You might need to traverse the object tree.
  • Be mindful of potential infinite loops when updating reactive data. A simple way to prevent this is to temporarily disable dependency tracking during updates.
  • Focus on the core reactivity logic. You don't need to implement a full-fledged component system. The goal is to understand how reactivity works.
  • Test your code thoroughly with various scenarios, including nested objects, property deletion, and multiple dependencies.
Loading editor...
typescript