Simple Reactivity

Core Concept

  • Dependency Tracking
  • Change Detection
  • Notification

Proxy

  • set “trap” for get and set
  • intercept property access and modification using proxy handler

const original = {name: "John"}
const proxy = new Proxy(original, {
  get(target, prop , receiver) {
    // trap `get` operation
    return Reflect.get(target, prop, receiver)
  },
  set(target, prop, value, receiver){
    // trap `set` operation
    Reflect.set(target, prop, value, receiver)
  }
})

Basic Usage


const store = reactive({ count: 1})

effect(() => {
    console.log(`Runs in Effect: count: ${store.count}`)
})

setInterval(() => {
    store.count++
}, 1000)


// Runs in Effect: count: 1
// Runs in Effect: count: 2
// Runs in Effect: count: 3
// ...

Implementation

// first we need containers for target and dependencies of key
const targetsMap = new WeakMap() // key -> target, value is depsMap

// variable for active(or current) effect. 
// we can access current effect using this.
let activeEffect = null // null or effect function


// let's create `effect` first. It just set activeEffect, run fn, and reset activeEffect.
function effect(fn) { 
  activeEffect = fn
  fn() // dependency tracking happens here. Consequntly, `track` will be called internally by proxy handler.
  activeEffect = null
}

// now let's create `track` and `trigger`
// `track` stores dependency (effect) for target and key
function track(target, key) {
  if (!activeEffect) return // if no active effect, do nothing
  let depsMap = targetsMap.get(target)
  if (!depsMap) {
    targetsMap.set(target, (depsMap = new Map())) // set default depsMap if not exists
  }

  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set())) // set default deps if not exists
  }

  deps.add(activeEffect) // add active effect to deps
}



// get deps for target and key, and run all deps
function trigger(target, key) {
  let depsMap = targetsMap.get(target)
  if (!depsMap) return // if no depsMap, do nothing

  let deps = depsMap.get(key)
  if (!deps) return // if no deps, do nothing

  deps.forEach(dep => dep()) // run all deps
}


// final `reactive` function. It just returns proxy object.
function reactive(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      const value = Reflect.get(target, prop, receiver)
      track(target, prop) // dependency tracking when property is accessed
      return value
    },
    set(target, prop, value, receiver) {
      const oldValue = target[prop]
      const result = Reflect.set(target, prop, value, receiver)
      if (oldValue !== value) {
        trigger(target, prop) // change detection and notification when property is modified
      }
      return result
    }
  })
}
graph TD
    A["Create reactive
wraps original object"] --> B["Set up Proxy"] B --> C["Install property traps"] C --> D{"trap type?"} D -->|get trap| E["Property accessed"] E --> F["Track effect
dependency"] F --> G["Store dependency
object.prop → effect"] D -->|set trap| H["Property modified"] H --> I["Trigger all tracked
effects for this property"] I --> J["Effect re-runs
component rerenders"] G --> K["state.value = x"] K --> H J --> L["Update DOM"]