Skip to content
Created: February 20, 2023

This part is the soul of the entire Vue3, more important than VDOM.

🍀 1. The core idea of Vue3 responsiveness (Proxy-based) ​

Vue2 uses Object.defineProperty, which has flaws:

  • Unable to monitor array subscripts
  • Unable to monitor new properties of object
  • Deep object dependencies are difficult to track
  • Performance is affected by large number of getters/setters

Vue3 was changed to Proxy to solve all the above pain points and support:

  • ✔ Deep monitoring
  • ✔ Array/Map/Set native monitoring
  • ✔ Lazy access
  • ✔ WeakMap caching to improve performance

Core structure:

state → Proxy(wrapper) → get → track → effect → trigger → update

🔥 2. Important components of responsiveness (the three core components) ​

The Vue3 reactivity package contains three cores:

① effect() ​

Function: Register side effect functions (such as component render)

The essence is:

Register the currently executed function as a "dependency" and re-execute it when the data changes.

② track() ​

Function: dependency collection Trigger scene: getter

When you visit:

js
state.count

Vue3 will call:

ts
track(target, key)

Bind the currently active effect to this key.

③ trigger() ​

Function: Distribute updates Trigger scene: setter

When executing:

js
state.count++

Vue will call:

ts
trigger(target, key)

Triggers all effects that depend on this key to re-execute (update the view).

🧠 3. Vue3 responsive data structure (very important) ​

The dependency collection relationship is as follows:

targetMap = WeakMap()
       ↓
target (state 对象)
       ↓
depsMap = Map()
       ↓
key → Dep(Set of effect)

It can be drawn like this:

WeakMap
└── target1 (reactive 对象)
      └── Map
           ├── key1 → Set(effectA, effectB)
           ├── key2 → Set(effectC)
└── target2
      └── ...

Explanation:

  • Each reactive object is a target
  • Each key corresponds to a dependency set Dep
  • Dep is a Set containing multiple effects.

🎯 4. Implementation mechanism of reactive() ​

Vue3's reactive() internally creates a Proxy:

ts
function reactive(target) {
  return new Proxy(target, mutableHandlers)
}

The most critical one is get/set:

get → track ​

ts
get(target, key, receiver) {
  const res = Reflect.get(target, key, receiver)
  track(target, key)
  return res
}

set → trigger ​

ts
set(target, key, value, receiver) {
  const oldValue = target[key]
  const result = Reflect.set(target, key, value, receiver)

  if (oldValue !== value) {
    trigger(target, key)
  }

  return result
}

There are only two things in the Proxy's handler:

get → track set → trigger

🚀 5. Effect core logic (relies on the collected brain) ​

effect(fn) will:

  1. Save fn to the global variable activeEffect
  2. Execute fn()
  3. Collect activeEffect when tracking

Source code pseudocode:

ts
let activeEffect = null

export function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn
    fn()
    activeEffect = null
  }
  effectFn()
  return effectFn
}

⚡ 6. track implementation (dependency collection) ​

ts
function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  dep.add(activeEffect)
}

Summary: Access data → Add the current effect to the dependency table

💥 7. Trigger implementation (dispatching updates) ​

When data changes:

ts
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effectFn => effectFn())
  }
}

The essence is:

All effects bound by key are re-executed (re-rendered)

🧨 8. How to avoid infinite loops in dependency tracking? ​

Vue 3 uses "scheduler" to avoid effect recursive triggering:

ts
effect(fn, {
  scheduler(job) {
    queueJob(job)
  }
})

Generally, the effects rendered by components will go through the scheduler to update them in batches and avoid repeated rendering.

📌 9. The essence of ref() ​

ref() is also a wrapper of reactive:

ts
export function ref(value) {
  const wrapper = {
    value
  }
  return reactive(wrapper)
}

That is to say:

js
const count = ref(0)

Equivalent to:

js
reactive({ value: 0 })

access count.value → get → track Modify count.value → set → trigger

📦 10. The internal mechanism of computed (very clever) ​

computed uses lazy effect + cache:

Pseudo code:

ts
function computed(getter) {
  let dirty = true
  let value

  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
    }
  })

  return {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      return value
    }
  }
}

computed features:

  • getter internal dependency change → dirty = true
  • The value will be recalculated next time it is accessed.
  • No calculation without access (lazy)

Very efficient.

🏆 11. Responsive complete process (super clear) ​

First render: ​

  1. effect(render)
  2. Access state.xxx → track in render
  3. Establish dependencies

Data changes: ​

  1. state.xxx = newValue → trigger
  2. Find all effects
  3. scheduler → queueJob
  4. Re-render (effect execution)
  5. patch → update DOM

Flowchart:

get → track → 收集依赖
set → trigger → effect → patch → DOM 更新

🔥 12. A mini-reactive implementation that you can run directly (core line 30) ​

js
let activeEffect = null
const targetMap = new WeakMap()

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn
    fn()
    activeEffect = null
  }
  effectFn()
}

function track(target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) targetMap.set(target, (depsMap = new Map()))
  let dep = depsMap.get(key)
  if (!dep) depsMap.set(key, (dep = new Set()))
  dep.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) dep.forEach(fn => fn())
}

function reactive(target) {
  return new Proxy(target, {
    get(t, k, r) {
      const res = Reflect.get(t, k, r)
      track(t, k)
      return res
    },
    set(t, k, v, r) {
      const old = t[k]
      const result = Reflect.set(t, k, v, r)
      if (old !== v) trigger(t, k)
      return result
    }
  })
}

You can use it:

js
const state = reactive({ count: 0 })

effect(() => {
  console.log("count changed:", state.count)
})

state.count++

🎓 13. At this point you have mastered: ​

  • ✔ reactive principle
  • ✔ effect / track / trigger
  • ✔ WeakMap → Map → Set data structure
  • ✔ ref/computed internal mechanism
  • ✔ scheduler scheduling system
  • ✔ Proxy interception logic

It has reached the level where you can view the Vue3 source code.