Frontend Development 13 min read

Understanding Vue's Reactive System: From Basic Effects to Proxy‑Based Reactivity

This article walks through the implementation of Vue's reactive system step by step, starting with simple effect functions, then introducing dependency collection with Set, Map, and WeakMap, and finally using Proxy to automate tracking and triggering of updates for both primitive and object values.

TAL Education Technology
TAL Education Technology
TAL Education Technology
Understanding Vue's Reactive System: From Basic Effects to Proxy‑Based Reactivity

Responsive programming is a core feature of React and Vue frameworks. This tutorial explains the underlying mechanisms by building a reactive system from scratch using plain JavaScript.

01 Basic version

The following code renders name and age on the page and manually calls effect functions after each change.

<body>
<div id="name"></div>
<div id="age"></div>
<script>
let name = 'hm'
let age = 23
const effect1 = () => document.getElementById('name').innerHTML = name
const effect2 = () => document.getElementById('age').innerHTML = age
effect1()
effect2()
name = 'HM'
age = 24
effect1()
effect2()
</script>
</body>

When many variables appear, repeatedly calling each effect becomes redundant. The next step collects effects.

02 Enhanced version – solving repeated effect execution

<body>
<div id="name"></div>
<div id="age"></div>
<script>
let name = 'hm'
let age = 23
const effect1 = () => document.getElementById('name').innerHTML = name
const effect2 = () => document.getElementById('age').innerHTML = age
let deps = new Set()
const track = (effect) => deps.add(effect)
const trigger = () => deps.forEach(effect => effect())
track(effect1)
track(effect2)
trigger()
</script>
</body>

All effects are stored in a Set and executed together via trigger() . This works for primitive values but fails for objects because a Set cannot differentiate which property changed.

03 Enhanced version – handling object properties

<body>
<div id="name"></div>
<div id="age"></div>
<script>
let person1 = { name: 'HM', age: 23 }
const effect1 = () => document.getElementById('name').innerHTML = person1.name
const effect2 = () => document.getElementById('age').innerHTML = person1.age
let depsMap = new Map()
const track = (key) => {
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
key === 'name' ? deps.add(effect1) : deps.add(effect2)
}
const trigger = (key) => {
let deps = depsMap.get(key)
if (deps) deps.forEach(effect => effect())
}
track('name')
track('age')
trigger('name')
trigger('age')
</script>
</body>

Using a Map keyed by property names allows separate effect collections for each field.

04 Enhanced version – supporting multiple objects

<body>
<div id="name"></div>
<div id="age"></div>
<script>
let person1 = { name: 'HM', age: 23 }
const effect1 = () => document.getElementById('name').innerHTML = person1.name
const effect2 = () => document.getElementById('age').innerHTML = person1.age
let targetMap = new WeakMap()
const track = (target, key) => {
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
key === 'name' ? deps.add(effect1) : deps.add(effect2)
}
const trigger = (target, key) => {
let depsMap = targetMap.get(target)
if (depsMap) {
let deps = depsMap.get(key)
if (deps) deps.forEach(effect => effect())
}
}
track(person1, 'name')
track(person1, 'age')
trigger(person1, 'name')
trigger(person1, 'age')
</script>
</body>

A WeakMap stores a map per object, enabling dependency tracking across multiple reactive objects.

05 Enhanced version – automatic dependency collection and triggering

<body>
<div id="name"></div>
<div id="age"></div>
<script>
const reactive = (target) => {
const handler = {
get(target, key, receiver) {
track(receiver, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key)
}
}
return new Proxy(target, handler)
}
let person1 = reactive({ name: 'HM', age: 23 })
const effect1 = () => document.getElementById('name').innerHTML = person1.name
const effect2 = () => document.getElementById('age').innerHTML = person1.age
let targetMap = new WeakMap()
const track = (target, key) => {
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}
const trigger = (target, key) => {
let depsMap = targetMap.get(target)
if (depsMap) {
let deps = depsMap.get(key)
if (deps) deps.forEach(effect => effect())
}
}
effect(effect1)
effect(effect2)
</script>
</body>

The reactive function creates a Proxy that automatically tracks reads via track and triggers stored effects on writes via trigger . The global activeEffect is set by an effect wrapper.

06 Final version – eliminating hard‑coded effects

<body>
<div id="name"></div>
<div id="age"></div>
<script>
let activeEffect
const effect = (fn) => {
activeEffect = fn
activeEffect()
activeEffect = null
}
const reactive = (target) => {
const handler = {
get(target, key, receiver) {
track(receiver, key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key)
}
}
return new Proxy(target, handler)
}
let person1 = reactive({ name: 'HM', age: 23 })
const effect1 = () => document.getElementById('name').innerHTML = person1.name
const effect2 = () => document.getElementById('age').innerHTML = person1.age
let targetMap = new WeakMap()
const track = (target, key) => {
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
}
const trigger = (target, key) => {
let depsMap = targetMap.get(target)
if (depsMap) {
let deps = depsMap.get(key)
if (deps) deps.forEach(effect => effect())
}
}
effect(effect1)
effect(effect2)
</script>
</body>

Now the effect to be collected is the currently running activeEffect , removing the need to hard‑code which effect belongs to which property.

Supplementary reading

Vue 3 implements ref and reactive using getters/setters rather than Proxy . The following snippet shows a simplified ref implementation.

const ref = (value) => {
class Ref {
#_value
constructor(value) { this.#_value = value }
get value() { track(this, 'value'); return this.#_value }
set value(newValue) { this.#_value = newValue; trigger(this, 'value') }
}
return new Ref(value)
}
let num = ref([1, 2])
effect(() => console.log(num.value))

Readers interested in deeper details can explore the Vue 3 source code at https://github.com/vuejs/core/blob/main/packages/reactivity/src/ref.ts .

frontendJavaScriptProxyVueReactiveDependencyTrackingweakmap
TAL Education Technology
Written by

TAL Education Technology

TAL Education is a technology-driven education company committed to the mission of 'making education better through love and technology'. The TAL technology team has always been dedicated to educational technology research and innovation. This is the external platform of the TAL technology team, sharing weekly curated technical articles and recruitment information.

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.