Frontend Development 24 min read

Understanding Vue 3 Reactivity: Ref Implementation, Dependency Tracking and Effect Optimization

This article explains Vue 3's reactivity mechanism by dissecting the ref API, showing how dependency collection and triggering work through effect functions, and describing the bit‑mask optimization introduced in Vue 3.2 to efficiently update only stale dependencies.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Understanding Vue 3 Reactivity: Ref Implementation, Dependency Tracking and Effect Optimization

Preface

This article analyzes Vue's reactivity using the ref API, focusing solely on the underlying mechanism without involving component concepts.

Vue's reactivity implementation resides in the @vue/reactivity package under packages/reactivity . For debugging the source, see reference [1].

Why use ref instead of reactive ?

ref is simpler, does not rely on ES6 Proxy , and only needs getter/setter functions, reducing learning cost.

What is Reactivity?

Definition from Vue 3 official docs [2].

Reactivity is a programming paradigm that lets code respond to changes declaratively, similar to how an Excel spreadsheet automatically updates a SUM when cell values change.

JavaScript does not behave this way by default; a simple example shows that updating a variable does not recompute dependent values.

We will examine Vue's test cases to see how reactivity is achieved.

ref Test Cases

The it block contains the test logic; we focus on the callback code.

it('should be reactive', () => {
    const a = ref(1)
    let dummy
    let calls = 0
    effect(() => {
        calls++
        dummy = a.value
    })
    expect(calls).toBe(1)
    expect(dummy).toBe(1)
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
    // same value should not trigger
    a.value = 2
    expect(calls).toBe(2)
    expect(dummy).toBe(2)
})

From the test we can conclude:

The function wrapped by effect runs automatically once.

When the ref value changes, the wrapped function re‑executes, providing reactivity.

Assigning the same value does not trigger a re‑execution.

What is effect ?

According to the official docs, effect creates a side‑effect object that tracks dependencies and re‑runs when they change.

We can imagine an updateDom function that updates the document body when a ref changes.

const a_ref = ref('aaaa')
function updateDom() {
    return document.body.innerText = a_ref.value
}
effect(updateDom)
setTimeout(() => {
    a_ref.value = 'bbb'
}, 1000)

Dependency Collection and Triggering Updates

Reactivity requires collecting dependencies (track) and triggering them (trigger) at the right moments.

Using the same test case, the appropriate moment is when a.value is modified.

When a ref is accessed, the current effect is recorded as a dependency.

export function trackRefValue(ref) {
    if (isTracking()) {
        ref = toRaw(ref)
        if (!ref.dep) {
            ref.dep = createDep()
        }
        trackEffects(ref.dep)
    }
}

The global flag shouldTrack and activeEffect determine whether collection should occur.

Only when an effect wraps the code do we collect dependencies; otherwise we skip.

The ref.dep is a Set<ReactiveEffect> that stores all side‑effect functions depending on that ref .

export function trackEffects(dep) {
    let shouldTrack = !dep.has(activeEffect!)
    if (shouldTrack) {
        dep.add(activeEffect!)
        activeEffect!.deps.push(dep)
    }
}

When the ref value changes, triggerRefValue looks up the stored dep and runs each effect.

export function triggerRefValue(ref, newVal) {
    ref = toRaw(ref)
    if (ref.dep) {
        triggerEffects(ref.dep)
    }
}

export function triggerEffects(dep) {
    for (const effect of isArray(dep) ? dep : [...dep]) {
        if (effect !== activeEffect || effect.allowRecurse) {
            if (effect.scheduler) {
                effect.scheduler()
            } else {
                effect.run()
            }
        }
    }
}

Recursive calls are prevented to avoid infinite loops.

effect Function and ReactiveEffect Object

The effect function creates a ReactiveEffect instance, runs it immediately, and returns a runner that can be called later.

export function effect(fn) {
    if ((fn as ReactiveEffectRunner).effect) {
        fn = (fn as ReactiveEffectRunner).effect.fn
    }
    const _effect = new ReactiveEffect(fn)
    _effect.run()
    const runner = _effect.run.bind(_effect)
    runner.effect = _effect
    return runner
}

The ReactiveEffect maintains a stack to support nested effects, enables tracking, cleans up old dependencies, runs the user function, and restores previous state.

export class ReactiveEffect {
    active = true
    deps = []
    constructor(public fn, public scheduler = null, scope) {
        recordEffectScope(this, scope)
    }
    run() {
        if (!effectStack.includes(this)) {
            try {
                effectStack.push((activeEffect = this))
                enableTracking()
                cleanupEffect(this)
                return this.fn()
            } finally {
                resetTracking()
                effectStack.pop()
                const n = effectStack.length
                activeEffect = n > 0 ? effectStack[n - 1] : undefined
            }
        }
    }
}

export function enableTracking() {
    trackStack.push(shouldTrack)
    shouldTrack = true
}

export function resetTracking() {
    const last = trackStack.pop()
    shouldTrack = last === undefined ? true : last
}

Dependency Update Optimization (Vue 3.2)

Vue 3.2 introduces a marker‑based optimization that marks each dep with “was” and “new” bits, allowing the engine to delete only stale dependencies instead of clearing all.

Each dep is a Set<ReactiveEffect> extended with numeric fields w and n . Bitwise operations set or clear bits according to the current effect nesting depth (up to 30 levels).

export const initDepMarkers = ({ deps }) => {
    if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
            deps[i].w |= trackOpBit
        }
    }
}

export const wasTracked = (dep) => (dep.w & trackOpBit) > 0
export const newTracked = (dep) => (dep.n & trackOpBit) > 0

If the nesting depth exceeds 30, Vue falls back to the original full‑cleanup strategy.

The optimized run method records the current depth, sets trackOpBit , marks all existing deps as “was”, runs the user function (which may mark some deps as “new”), then removes deps that have “was” but not “new”.

run() {
    if (!effectStack.includes(this)) {
        try {
            effectStack.push((activeEffect = this))
            trackOpBit = 1 << ++effectTrackDepth
            if (effectTrackDepth <= maxMarkerBits) {
                initDepMarkers(this)
            } else {
                cleanupEffect(this)
            }
            return this.fn()
        } finally {
            if (effectTrackDepth <= maxMarkerBits) {
                finalizeDepMarkers(this)
            }
            trackOpBit = 1 << --effectTrackDepth
            // restore previous activeEffect etc.
        }
    }
}

export const finalizeDepMarkers = (effect) => {
    const { deps } = effect
    if (deps.length) {
        let ptr = 0
        for (let i = 0; i < deps.length; i++) {
            const dep = deps[i]
            if (wasTracked(dep) && !newTracked(dep)) {
                dep.delete(effect)
            } else {
                deps[ptr++] = dep
            }
            dep.w &= ~trackOpBit
            dep.n &= ~trackOpBit
        }
        deps.length = ptr
    }
}

References

Vue official documentation [6]

Vue‑next source code [7]

Conclusion

If this article helped you, please give it a thumbs‑up; your encouragement fuels further writing.

JavaScriptVueDependency TrackingReactivityrefeffect
Sohu Tech Products
Written by

Sohu Tech Products

A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.