This document provides an overview of the signal-based reactivity system implemented in this project. This system allows for efficient and fine-grained updates in response to data changes. It is inspired by reactive programming paradigms and utilizes key concepts like signals, derived values, and effects.
The reactivity system revolves around three main concepts:
-
Signals (Sources): These are the fundamental building blocks of the system. A signal holds a value that can change over time. When a signal's value is updated, it notifies all the parts of the system that depend on it.
-
Derived Values (Deriveds): These are signals whose values are automatically computed based on the values of other signals. When the dependencies of a derived value change, its value is automatically recomputed.
-
Effects: These are functions that run in response to changes in signals. They are used to perform side effects, such as updating the DOM, logging data, or making API calls, whenever the signals they depend on change.
This reactivity system is defined by the following files:
This file contains the logic for creating and managing derived signals. Derived signals are read-only signals whose values are derived from other signals.
Key Functions:
derived(fn): Creates a new derived signal. The provided functionfnis executed to compute the derived value. This function automatically tracks any other signals accessed within it. When any of these dependency signals change, thefnwill be re-executed, and the derived signal's value will be updated.- It handles scenarios where derived signals are created within other reactions (deriveds or effects) to establish ownership and dependency trees.
- It uses flags to track the state of the derived signal (e.g.,
DIRTY,UNOWNED).
user_derived(fn): Similar toderived, but it also pushes the newly created derived signal onto a runtime stack. This is likely used for specific lifecycle management or initial value tracking, especially in the context of components.derived_safe_equal(fn): Creates a derived signal that uses a "safe equality" check instead of strict equality to determine if the derived value has changed. This is useful when comparing complex objects where reference equality might not be sufficient.destroy_derived_effects(derived): When a derived signal is no longer needed, this function cleans up any effects that were created inside the derived's computation function. This helps prevent memory leaks and ensures proper resource management.update_derived(derived): This internal function is responsible for re-evaluating the derived signal's computation function. It handles dependency tracking, checks if the new value is different from the old one, updates the derived signal's value and write version, and marks any dependent reactions as needing updates.- It also includes logic to detect and prevent infinite recursion in derived computations.
This file defines how effects are created and managed. Effects are used to perform side effects in response to signal changes.
Key Functions:
user_effect(fn): Creates a new effect. The provided functionfnis executed initially and whenever any of the signals it depends on change.- It includes validation to ensure effects are created within a valid reactive context (e.g., not orphaned or during teardown).
- It handles deferring the execution of effects within certain contexts (like component mounting).
user_pre_effect(fn): Creates an effect that is intended to run synchronously before other effects, often used for setup or tasks that need to happen immediately.inspect_effect(fn): Creates a special effect used for debugging and introspection of the reactivity system. These effects run immediately when their dependencies change.effect_root(fn): Creates a root effect. Any effects created within the provided functionfnare considered children of this root effect. When the root effect is destroyed, all its child effects are also destroyed, providing a mechanism for managing the lifecycle of related effects.component_root(fn): Similar toeffect_root, but specifically designed for managing effects within a component. It includes functionality for handling outro transitions before destroying child effects.effect(fn): A lower-level function for creating effects with specific flags and configurations.legacy_pre_effect(deps, fn)andlegacy_pre_effect_reset(): These functions seem to be related to a previous or legacy reactivity system and are likely kept for compatibility or specific use cases.render_effect(fn): Creates an effect that is specifically intended for rendering updates. These effects typically run synchronously.template_effect(fn, thunks, d = derived): Creates an effect that depends on a set of derived values (represented bythunks). This is likely used in the context of rendering dynamic content in templates.block(fn, flags = 0): Creates a block effect, which might have specific characteristics or optimizations related to rendering blocks of content.branch(fn, push = true): Creates a branch effect, likely used for conditional rendering logic.execute_effect_teardown(effect): If an effect has a teardown function (a function returned by the main effect function), this function executes it when the effect is being destroyed. This is used for cleanup tasks like removing event listeners.destroy_effect_children(signal, remove_dom = false): Recursively destroys all child effects of a given effect. It can optionally remove associated DOM nodes.destroy_block_effect_children(signal): Destroys child effects of a block effect, potentially excluding certain types of effects (like branch effects).destroy_effect(effect, remove_dom = true): Destroys a specific effect. This involves executing its teardown function, destroying its child effects, removing associated DOM nodes (if applicable), and unlinking it from the reactive graph.unlink_effect(effect): Detaches an effect from its parent in the effect tree.pause_effect(effect, callback)andresume_effect(effect): These functions are used to temporarily pause and resume the execution of an effect and its children, often used in conjunction with animations or transitions.
This file defines the fundamental source signals, which hold mutable values and trigger updates when their values change.
Key Functions:
source(v, stack): Creates a new source signal with an initial valuev. This is the most basic type of signal that directly holds a mutable value.- In development mode with tracing enabled, it can capture the creation stack for debugging purposes.
state(v, stack): Similar tosource, but it also pushes the newly created signal onto a runtime stack. This is likely the primary way to create mutable state within components or the application.mutable_source(initial_value, immutable = false): Creates a source signal that is intended to be mutated. Theimmutableflag can be used to control whether the signal uses strict equality checks for updates.- It also handles binding the signal to the component context in legacy mode for lifecycle callbacks.
mutate(source, value): Provides a way to update a source signal's value by applying a mutation to its current value, while ensuring that the update triggers reactivity.set(source, value, should_proxy = false): Updates the value of a source signal to the providedvalue.- It includes checks to prevent unsafe mutations of state within derived computations or block effects in runes mode.
- It uses an equality check to avoid triggering updates if the new value is the same as the old one (using the signal's
equalsmethod). - It increments a write version counter to track changes.
- It marks all dependent reactions (deriveds and effects) as dirty.
- It handles immediate updates for inspect effects.
internal_set(source, value): A lower-level function that performs the actual setting of the source signal's value and handles the update logic.update(source, d = 1)andupdate_pre(source, d = 1): Convenience functions for incrementing or decrementing the numerical value of a source signal.mark_reactions(signal, status): This internal function iterates through all the reactions (deriveds and effects) that depend on the givensignaland marks them with the specifiedstatus(e.g.,DIRTYorMAYBE_DIRTY), indicating that they need to be re-evaluated or re-run.
This file deals with how component properties (props) are handled within the reactivity system, allowing data to flow into components and be reactive.
Key Functions:
prop(props, key, flags, fallback): This is the core function for defining a component property.- It takes the component's props object, the key of the property, flags indicating its behavior (e.g.,
PROPS_IS_IMMUTABLE,PROPS_IS_BINDABLE,PROPS_IS_RUNES,PROPS_IS_UPDATED), and an optional fallback value. - It handles cases where the prop might be bound (using
bind:syntax), in which case it captures the store binding. - It provides a getter function to access the current value of the prop.
- If the prop is marked as
PROPS_IS_UPDATED, it returns a setter function that allows the component to update the prop's value, which will then propagate back to the parent component if it's bound. - It handles synchronization between the parent and child component's prop values, including logic for local temporary overrides.
- It also manages fallback values when the prop is not provided by the parent.
- It takes the component's props object, the key of the property, flags indicating its behavior (e.g.,
rest_props(props, exclude, name)andlegacy_rest_props(props, exclude): These functions handle the collection of "rest" props (props passed to a component that are not explicitly declared). They return a proxy that allows accessing these props. The legacy version has additional logic for handling updates and deletions.spread_props(...props): This function takes an array of prop objects (or functions that return prop objects) and creates a proxy that merges them into a single props object. This is used for the spread syntax in templates.update_prop(fn, d = 1)andupdate_pre_prop(fn, d = 1): These utility functions are similar to theupdatefunctions insources.jsbut are specifically designed for updating prop values.
Here are some basic examples to illustrate how to use this reactivity system:
Creating a Source Signal:
import { state } from './sources.js';
const count = state(0); // Creates a signal with an initial value of 0
console.log(count.v); // Access the current value (0)
Updating a Source Signal:
import { set } from './sources.js';
import { count } from './examples'; // Assuming the above example
set(count, 1); // Updates the value of the count signal to 1
console.log(count.v); // Output: 1
Creating a Derived Signal:
import { derived } from './deriveds.js';
import { count } from './examples'; // Assuming the above example
const doubleCount = derived(() => count.v * 2);
console.log(doubleCount.v); // Output: 2 (based on the current count value of 1)
Creating an Effect:
import { user_effect } from './effects.js';
import { count } from './examples'; // Assuming the above example
user_effect(() => {
console.log(`Count is: ${count.v}`); // This will log "Count is: 1" when count changes
});
// When count is updated:
import { set } from './sources.js';
set(count, 2); // This will trigger the effect, logging "Count is: 2"
Component Property:
// In a component's code:
import { prop } from './props.js';
export function MyComponent(props) {
const name = prop(props, 'name');
// Access the prop value:
console.log(name());
}
// When using the component:
<MyComponent name="Alice" />;
Key Features and Principles
* Fine-Grained Dependency Tracking: The system automatically tracks which signals are used within derived computations and effect functions. This ensures that updates are only triggered when the relevant dependencies change.
* Lazy Evaluation: Derived signals are typically evaluated lazily, meaning their values are only computed when they are actually needed by other parts of the system (like effects or when their value is explicitly accessed).
* Efficient Updates: When a source signal changes, only the derived signals and effects that directly depend on it are re-evaluated or re-run, leading to efficient and minimal updates.
* Reactive Context Management: The system uses a concept of an "active reaction" and "active effect" to manage the context in which derived signals and effects are created, ensuring proper dependency tracking and ownership.
* Lifecycle Management: Features like effect roots and component roots provide mechanisms for managing the lifecycle of effects and ensuring they are properly cleaned up when they are no longer needed.
This documentation provides a foundational understanding of the signal-based reactivity system. For more detailed information on specific functions or advanced use cases, please refer to the source code and any additional documentation.
I had Gemini 2.5 and ChatGPT o1 Pro explain the result of building with Vite using only the minimum necessary reactive code.