← articlesripple internals

Ripple Runtime: Mount & Block Tree

Part 3 of the Ripple internals deep dive series.

Phase 2: Initial Execution - Component Mount

Now that we've seen how the code is compiled, let's trace what happens when the component actually executes. This is where the runtime system comes alive, creating tracked values, building the block tree, and registering dependencies.

Step 2.1: Component Function Call

When we mount the component:

mount(Counter, { target: document.getElementById('root') });

Global Runtime State (Initial):

// These global variables track the current execution context
active_component = null      // No component active yet
active_block = null          // No block executing
active_reaction = null       // No reactive computation running
tracking = false             // Dependency tracking disabled
clock = 0                    // Global clock for change detection
queued_root_blocks = []      // Empty update queue
old_values = new Map()       // Empty map for teardown values

These global variables form the execution context. As blocks execute, these variables change to track what's currently happening. When a block reads a tracked value with _$_.get(), it uses active_reaction to know where to register the dependency.

Step 2.2: Tracked Value Creation

When we execute let count = track(0, ...), we're creating a reactive value. Let's see exactly what happens.

Code:

let count = track(0, undefined, undefined, component_ctx);

Algorithm: track(value, get, set, block)

FUNCTION track(0, undefined, undefined, component_ctx):
    // Step 1: Check if already tracked (idempotent)
    IF is_tracked_object(0):  // false - 0 is a primitive
        RETURN value

    // Step 2: Validate block exists
    IF component_ctx IS NULL:  // false
        THROW TypeError('track() requires a valid component context')

    // Step 3: Check if value is a function (derived value)
    IF typeof 0 === 'function':  // false
        RETURN derived(...)

    // Step 4: Create simple tracked value
    RETURN tracked(0, component_ctx, undefined, undefined)
END FUNCTION

Algorithm: tracked(value, block, get, set)

FUNCTION tracked(0, component_ctx, undefined, undefined):
    RETURN {
        __v: 0,                    // Current value
        a: {                       // Accessors (get/set functions)
            get: undefined,
            set: undefined
        },
        b: component_ctx,          // Associated block (for scheduling updates)
        c: 0,                     // Clock value (incremented on change)
        f: TRACKED                // Flags (TRACKED = simple value, not derived)
    }
END FUNCTION

Result - count object:

count = {
    __v: 0,                    // The actual value
    a: { get: undefined, set: undefined },
    b: component_ctx,          // Which block to update when this changes
    c: 0,                      // Clock value (starts at 0)
    f: TRACKED                 // Flag: this is a simple tracked value
}

Result - showDouble object:

showDouble = {
    __v: true,                 // The actual value
    a: { get: undefined, set: undefined },
    b: component_ctx,          // Same block association
    c: 0,                      // Clock value
    f: TRACKED                 // Simple tracked value
}

Key Points:

  • __v stores the actual value (0 or true)
  • b (block) is crucial - when this value changes, we'll schedule an update to this block
  • c (clock) starts at 0 and increments every time the value changes
  • f (flags) tells us this is a simple tracked value, not a computed one

Tracked Value Structure:

Tracked Value Structure

Now let's see how the derived value double is created, which has a different structure because it's computed rather than stored directly.

Step 2.3: Derived Value Creation

Now for the interesting one - double is a derived value. It's computed from count, not stored directly.

Code:

let double = track(() => _$_.get(count) * 2, undefined, undefined, component_ctx);

Notice: We pass a function, not a value. This tells Ripple "compute this value when needed."

Algorithm: track(fn, get, set, block)

FUNCTION track(fn, undefined, undefined, component_ctx):
    // fn = () => _$_.get(count) * 2

    // Check if it's a function (derived value)
    IF typeof fn === 'function':  // true
        RETURN derived(fn, component_ctx, undefined, undefined)

    // Otherwise create simple tracked value
    RETURN tracked(fn, component_ctx, undefined, undefined)
END FUNCTION

Algorithm: derived(fn, block, get, set)

FUNCTION derived(fn, component_ctx, undefined, undefined):
    RETURN {
        __v: UNINITIALIZED,      // Not computed yet! (lazy)
        a: { get: undefined, set: undefined },
        b: component_ctx,         // Associated block
        blocks: null,            // Child blocks (created during computation)
        c: 0,                    // Clock value
        co: active_component,     // Component context
        d: null,                 // Dependency chain (empty - will be populated)
        f: TRACKED | DERIVED,    // Flags: both TRACKED and DERIVED
        fn: fn                   // Computation function
    }
END FUNCTION

Result - double object:

double = {
    __v: UNINITIALIZED,          // Not computed yet! (lazy evaluation)
    a: { get: undefined, set: undefined },
    b: component_ctx,
    blocks: null,                // Will hold child blocks created during computation
    c: 0,                        // Clock value
    co: null,                    // Component context
    d: null,                     // No dependencies yet (will track count)
    f: TRACKED | DERIVED,        // Both flags set
    fn: () => _$_.get(count) * 2 // The computation function
}

Key Differences from Simple Tracked Values:

PropertySimple (count)Derived (double)
__vHas value immediately (0)UNINITIALIZED (lazy)
fTRACKEDTRACKED | DERIVED
fnundefinedComputation function
dnull (not used)null (will track dependencies)

Derived values aren't computed until someone reads them. This lazy evaluation avoids unnecessary computation. When we first access @double, that's when double.fn() runs and double.d gets populated with dependencies.

Derived Value Structure:

Derived Value Structure

With our tracked values created, the next step is to create the root block that will contain all the rendering logic.

Step 2.4: Root Block Creation

Blocks are the fundamental unit of reactive execution in Ripple. Think of them as "reactive functions" - they execute, track dependencies, and re-execute when those dependencies change.

Code:

return _$_.root(() => { /* render function */ }, component_ctx);

Algorithm: root(fn, compat, component_ctx)

FUNCTION root(fn, undefined, component_ctx):
    // Create a root block (top-level block for component)
    RETURN block(ROOT_BLOCK, fn, { compat: undefined }, component_ctx)
END FUNCTION

Algorithm: block(flags, fn, state, co)

FUNCTION block(ROOT_BLOCK, fn, { compat }, component_ctx):
    block = {
        co: component_ctx,    // Component context
        d: null,              // Dependency chain (empty initially)
        first: null,          // First child block
        f: ROOT_BLOCK,        // Flags (ROOT_BLOCK = top-level)
        fn: fn,               // Function to execute
        last: null,           // Last child block
        next: null,           // Next sibling block
        p: null,              // Parent block (null for root)
        s: { compat },        // State (DOM nodes, etc.)
        t: null               // Teardown function
    }

    // Link to parent if one exists (none for root)
    IF active_block IS NOT NULL:
        push_block(block, active_block)

    RETURN block
END FUNCTION

Result - root_block structure:

root_block = {
    co: component_ctx,        // Which component owns this block
    d: null,                  // Dependency chain (will be populated)
    first: null,              // Will point to first child block
    f: ROOT_BLOCK,           // Flag: this is a root block
    fn: () => { /* render function */ },  // What to execute
    last: null,               // Will point to last child block
    next: null,               // No siblings
    p: null,                  // No parent (it's the root!)
    s: { compat: undefined }, // State storage
    t: null                   // Teardown function (set later)
}

Block Structure Explained:

PropertyPurposeExample
coComponent contextLinks block to component
dDependency chainTracks which values this block depends on
first/lastChild blocksForms a tree structure
fFlagsROOT_BLOCK, RENDER_BLOCK, BRANCH_BLOCK, etc.
fnExecution functionWhat code to run
pParent blockLinks to parent in tree
sStateStores DOM nodes, data, etc.
tTeardownCleanup function

Blocks form a tree structure where the root block contains everything, and child blocks handle specific parts like rendering text or conditionals.

Block Tree Structure:

Block Tree Structure

Now that the root block is created, it needs to execute to render the component.

Step 2.5: Root Block Execution

Now the root block executes. This is where the component actually renders!

Code:

run_block(root_block);

Context Before Execution:

active_block = null
active_reaction = null
tracking = false
active_dependency = null
active_component = null

Algorithm: run_block(root_block)

FUNCTION run_block(root_block):
    // Step 1: Save current context (for nested execution)
    previous_block = active_block          // null
    previous_reaction = active_reaction    // null
    previous_tracking = tracking           // false
    previous_dependency = active_dependency // null
    previous_component = active_component   // null

    TRY:
        // Step 2: Set active context
        active_block = root_block          // Now this block is active
        active_reaction = root_block       // This is the current reaction
        active_component = root_block.co   // Set component context

        // Step 3: Cleanup (none needed on first run)
        destroy_non_branch_children(root_block)  // No children yet
        run_teardown(root_block)                 // No teardown yet

        // Step 4: Enable tracking? NO - ROOT_BLOCK doesn't track!
        tracking = (root_block.f & (ROOT_BLOCK | BRANCH_BLOCK)) === 0
        // Since root_block.f has ROOT_BLOCK flag:
        // tracking = false  // Important: root blocks don't track dependencies

        active_dependency = null

        // Step 5: Execute the render function!
        result = root_block.fn(root_block.s)
        // This creates child blocks, renders DOM, etc.

        // Step 6: Store teardown function if returned
        IF typeof result === 'function':
            root_block.t = result  // Save for cleanup later

            // Mark parent blocks as containing teardown (none for root)
            current = root_block
            WHILE current IS NOT NULL AND (current.f & CONTAINS_TEARDOWN) === 0:
                current.f = current.f | CONTAINS_TEARDOWN
                current = current.p

        // Step 7: Store dependency chain
        root_block.d = active_dependency  // null (tracking was false)

    FINALLY:
        // Step 8: Restore previous context
        active_block = previous_block
        active_reaction = previous_reaction
        tracking = previous_tracking
        active_dependency = previous_dependency
        active_component = previous_component
END FUNCTION

Context After Execution:

active_block = null          // Restored
active_reaction = null       // Restored
tracking = false             // Restored
active_dependency = null     // Restored
active_component = null      // Restored

Key Points:

  1. Context Save/Restore: This allows nested block execution. When a child block runs, it can save/restore context safely.

  2. Root Blocks Don't Track: tracking = false means root blocks don't register dependencies. They always execute when scheduled.

  3. Dependency Chain: root_block.d = null because tracking was disabled. Child blocks will have dependency chains.

Root blocks are the entry point - they always execute when their component updates. Child blocks are the ones that need fine-grained tracking. Now let's see what happens inside the render function as it creates child blocks.

Step 2.6: Template Creation

Inside the render function:

const __template0 = _$_.template('<div><p><!></p><!></div>');
const __fragment = __template0();

Algorithm: template(content, flags)

FUNCTION template('<div><p><!></p><!></div>', flags):
    node = undefined  // Cached template

    RETURN () => {
        IF node === undefined:
            // Create template element
            elem = document.createElement('template')
            elem.innerHTML = '<div><p><!></p><!></div>'
            node = elem.content  // Cache it

        // Clone template
        clone = node.cloneNode(true)

        // Assign start/end nodes
        assign_nodes(first_child(clone), clone.lastChild)

        RETURN clone
    }
END FUNCTION

Result: DOM fragment with placeholder comment nodes (<!>) for dynamic content.

Step 2.7: Count Text Rendering - Dependency Registration

This is where dependency tracking actually happens. When we render the count text, we read @count, which triggers the dependency registration system. This is the critical moment where blocks learn which tracked values they depend on.

Code:

_$_.render(() => {
    _$_.set_text(__text0.firstChild, 'Count: ' + _$_.get(count));
});

What render() does:

FUNCTION render(fn, null, 0):
    // Creates a RENDER_BLOCK (not ROOT_BLOCK!)
    RETURN block(RENDER_BLOCK, fn, null)
END FUNCTION

Created Block:

count_render_block = {
    co: component_ctx,
    d: null,              // Will be populated during execution!
    first: null,
    f: RENDER_BLOCK,      // ← Different flag from ROOT_BLOCK
    fn: () => {
        _$_.set_text(__text0.firstChild, 'Count: ' + _$_.get(count));
    },
    last: null,
    next: null,
    p: root_block,        // ← Parent is root block
    s: null,
    t: null
}

Context Before Execution:

active_block = root_block
active_reaction = root_block
tracking = false          // Root block doesn't track
active_dependency = null

Execution: run_block(count_render_block)

FUNCTION run_block(count_render_block):
    // Save context
    previous_block = active_block          // root_block
    previous_reaction = active_reaction    // root_block
    previous_tracking = tracking           // false
    previous_dependency = active_dependency // null

    TRY:
        // Set new context
        active_block = count_render_block      // Now this block is active
        active_reaction = count_render_block   // This is the reaction
        active_component = component_ctx

        // Enable tracking! (RENDER_BLOCK is not ROOT_BLOCK)
        tracking = (count_render_block.f & (ROOT_BLOCK | BRANCH_BLOCK)) === 0
        // RENDER_BLOCK doesn't have ROOT_BLOCK or BRANCH_BLOCK flags
        // tracking = true  // Now dependencies will be tracked!

        active_dependency = null  // Will be built during execution

        // Execute the function - this is where dependency registration happens
        result = count_render_block.fn()
        // Inside: _$_.get(count) is called

        // Store the dependency chain
        count_render_block.d = active_dependency  // Now has count dependency!

    FINALLY:
        // Restore context
        active_block = previous_block
        active_reaction = previous_reaction
        tracking = previous_tracking
        active_dependency = previous_dependency
END FUNCTION

Context During Execution:

active_block = count_render_block    // Changed!
active_reaction = count_render_block // Changed!
tracking = true                      // Enabled!
active_dependency = null             // Will be built

Inside count_render_block.fn(): _$_.get(count) is called

This is the critical moment! Let's trace it step by step:

Step 1: get(count)

FUNCTION get(count):
    // Check if it's a tracked object
    IF NOT is_tracked_object(count):  // false - count IS tracked
        RETURN count

    // Check if it's derived
    IF (count.f & DERIVED) !== 0:  // false - count.f = TRACKED, not DERIVED
        RETURN get_derived(count)

    // It's a simple tracked value
    RETURN get_tracked(count)  // ← Go here!
END FUNCTION

Step 2: get_tracked(count)

FUNCTION get_tracked(count):
    // Step 1: Get the value
    value = count.__v  // 0

    // Step 2: Register dependency (THIS IS THE KEY!)
    IF tracking:  // true (enabled by RENDER_BLOCK)
        register_dependency(count)  // ← This creates the link!

    // Step 3: Handle teardown (not applicable here)
    IF teardown AND old_values.has(count):  // false
        value = old_values.get(count)

    // Step 4: Apply custom getter (none)
    IF count.a.get IS NOT undefined:  // false
        value = trigger_track_get(count.a.get, value)

    RETURN value  // 0
END FUNCTION

Step 3: register_dependency(count) - The Dependency Link! 🔗

This is where count_render_block learns it depends on count:

FUNCTION register_dependency(count):
    // active_reaction = count_render_block (set by run_block)
    // active_dependency = null (initial state)

    dependency = active_dependency  // null

    // Create first dependency
    IF dependency IS NULL:  // true
        dependency = create_dependency(count)
        active_dependency = dependency  // ← Store in global
        RETURN
END FUNCTION

Step 4: create_dependency(count) - Create the Link Node

FUNCTION create_dependency(count):
    reaction = active_reaction  // count_render_block
    existing = reaction.d       // null (no dependencies yet)

    // Try to recycle (none available)
    IF existing IS NOT NULL:  // false
        // Recycle logic...
        RETURN existing

    // Create new dependency node
    RETURN {
        c: count.c,      // 0 ← Clock value when registered
        t: count,        // ← Reference to tracked value
        n: null          // ← Next dependency (none yet)
    }
END FUNCTION

Result - Dependency Chain Created:

// Global state updated
active_dependency = {
    c: 0,        // count's clock value (0)
    t: count,     // Reference to count object
    n: null       // No next dependency
}

// Block's dependency chain stored
count_render_block.d = {
    c: 0,
    t: count,
    n: null
}

Context After Execution:

active_block = root_block          // Restored
active_reaction = root_block       // Restored
tracking = false                   // Restored
active_dependency = null          // Restored

Final State:

// The block now knows it depends on count!
count_render_block = {
    // ...
    d: {                    // Dependency chain!
        c: 0,               // Clock value when registered
        t: count,            // Which value it depends on
        n: null              // No other dependencies
    }
}

// DOM updated
// Text node now shows: "Count: 0"

What Just Happened:

  1. Block executed with tracking = true
  2. _$_.get(count) called → reads value 0
  3. Dependency registeredcount_render_block.d now points to count
  4. Clock value storedc: 0 (we'll compare this later to detect changes)

The dependency chain count_render_block.d is a linked list that says: "This block depends on count, and it was registered when count's clock was 0."

Dependency Chain Structure:

Dependency Chain Structure

Later, when count changes, its clock increments. We can check count.c > dependency.c to see if it changed. This clock comparison is the core of Ripple's efficient change detection.

Now let's see how the conditional block for showDouble works, which will also register dependencies.

Step 2.8: Double Conditional Block

_$_.if(__anchor1, (set_branch) => {
    if (_$_.get(showDouble)) {
        set_branch((anchor) => { /* render double */ });
    }
});

Algorithm: if_block(node, fn)

FUNCTION if_block(__anchor1, fn):
    anchor = __anchor1
    has_branch = false
    condition = UNINITIALIZED
    b = null  // Branch block

    set_branch = (fn, flag = true) => {
        has_branch = true
        update_branch(flag, fn)
    }

    update_branch = (new_condition, fn) => {
        IF condition === new_condition:  // Skip if unchanged
            RETURN

        // Destroy old branch
        IF b !== null:
            destroy_block(b)
            b = null

        // Create new branch if condition is truthy
        IF fn !== null:
            b = branch(() => fn(anchor))
    }

    // Render block that evaluates condition
    render(() => {
        has_branch = false
        fn(set_branch)  // Calls our function with set_branch
        IF NOT has_branch:
            update_branch(null, null)
    }, null, IF_BLOCK)
END FUNCTION

Execution:

  1. render() creates an IF_BLOCK
  2. Block executes: fn(set_branch) is called
  3. Inside: if (_$_.get(showDouble)) evaluates

_$_.get(showDouble):

FUNCTION get(showDouble):
    RETURN get_tracked(showDouble)
END FUNCTION

FUNCTION get_tracked(showDouble):
    value = showDouble.__v  // true

    // Register dependency
    IF tracking:  // true
        register_dependency(showDouble)

    RETURN value  // true
END FUNCTION

Dependency registered:

if_block.d = {
    c: 0,
    t: showDouble,
    n: null
}

Since the condition is true, set_branch() is called, which creates a branch block. This branch block will render the double value. Inside the branch block, when we access @double, this triggers the first computation of the derived value.

Branch block execution:

branch_block = {
    co: component_ctx,
    d: null,
    f: BRANCH_BLOCK,
    fn: (anchor) => {
        const __template1 = _$_.template('<p><!></p>');
        const __fragment1 = __template1();
        anchor.before(__fragment1);

        _$_.render(() => {
            _$_.set_text(__fragment1.firstChild, 'Double: ' + _$_.get(double));
        });
    },
    p: if_block,
    // ...
}

Inside branch: _$_.get(double)

This is the first access to the derived value, which means it needs to be computed for the first time.

Algorithm: get(double)

FUNCTION get(double):
    // double is derived
    IF (double.f & DERIVED) !== 0:  // true
        RETURN get_derived(double)
END FUNCTION

Algorithm: get_derived(double)

FUNCTION get_derived(double):
    // Update derived value
    update_derived(double)

    // Register dependency
    IF tracking:  // true
        register_dependency(double)

    // No custom getter
    IF double.a.get IS NOT undefined:  // false
        double.__v = trigger_track_get(double.a.get, double.__v)

    RETURN double.__v
END FUNCTION

Algorithm: update_derived(double)

FUNCTION update_derived(double):
    value = double.__v  // UNINITIALIZED

    // Recompute (uninitialized)
    IF value === UNINITIALIZED OR is_tracking_dirty(double.d):  // true
        value = run_derived(double)

        // Update if changed
        IF value !== double.__v:  // true
            double.__v = value      // 0
            double.c = increment_clock()  // 1
END FUNCTION

Algorithm: run_derived(double)

FUNCTION run_derived(double):
    // Save context
    previous_block = active_block          // branch_block
    previous_reaction = active_reaction    // branch_block
    previous_tracking = tracking           // true
    previous_dependency = active_dependency // null
    previous_component = active_component   // component_ctx
    previous_is_mutating_allowed = is_mutating_allowed  // true

    TRY:
        // Set context for computation
        active_block = null
        active_reaction = double  // Derived value is the reaction!
        active_component = component_ctx
        tracking = true           // Enable dependency tracking
        active_dependency = null
        is_mutating_allowed = false  // Prevent mutations

        // Destroy old child blocks (none yet)
        destroy_computed_children(double)

        // Run computation: () => _$_.get(count) * 2
        value = double.fn()

        // Inside fn(): _$_.get(count)
        // This registers count as a dependency of double!

        // No custom getter
        IF double.a.get IS NOT undefined:  // false
            value = trigger_track_get(double.a.get, value)

        RETURN value  // 0
END FUNCTION

Inside double.fn(): _$_.get(count)

FUNCTION get_tracked(count):
    value = count.__v  // 0

    // Register dependency (tracking === true, active_reaction === double)
    IF tracking:  // true
        register_dependency(count)  // Registers to double, not branch_block!

    RETURN value  // 0
END FUNCTION

Dependency registered to double:

double.d = {
    c: 0,  // count's clock value
    t: count,
    n: null
}

After computation:

double = {
    __v: 0,  // Computed value
    c: 1,    // Clock incremented
    d: {
        c: 0,
        t: count,
        n: null
    }
    // ...
}

Back to get_derived(double):

// Register double as dependency of branch_block
branch_block.d = {
    c: 1,  // double's clock value
    t: double,
    n: null
}

Final state after mount:

count = { __v: 0, c: 0, d: null, ... }
showDouble = { __v: true, c: 0, d: null, ... }
double = { __v: 0, c: 1, d: { c: 0, t: count, n: null }, ... }

count_render_block.d = { c: 0, t: count, n: null }
if_block.d = { c: 0, t: showDouble, n: null }
branch_block.d = { c: 1, t: double, n: null }
double.d = { c: 0, t: count, n: null }

Complete Dependency Graph After Mount:

Complete Dependency Graph