Vue3 Reactive System: Dependency Tracking and Update Triggering Principles
Problem Description
Understand the core mechanisms of dependency tracking (track) and update triggering (trigger) in Vue3's reactive system, including how to establish associations between reactive data and side-effect functions, and how to precisely notify relevant dependencies when data changes.
Knowledge Explanation
1. Core Goals of the Reactive System
- When reactive data changes, automatically execute side-effect functions that depend on that data (such as component render functions, computed properties, watch listeners, etc.)
- Need to establish a mapping relationship between data properties and side-effect functions to achieve precise updates
2. Basic Concept Definitions
Side-Effect Function (Effect)
// Any function that reads reactive data is a side-effect function
const effect = () => {
document.body.innerText = obj.text // Reading obj.text
}
Dependency Relationship (Deps Map)
- Uses a three-level mapping structure:
TargetMap: WeakMap, where keys are reactive objects and values are DepsMapsDepsMap: Map, where keys are object property names and values are Dep SetsDep: Set, storing all side-effect functions that depend on this property
3. Dependency Tracking (Track) Process
Step 1: Establish Data Structure
const targetMap = new WeakMap() // Global dependency storage
function track(target, key) {
if (!activeEffect) return // Do not track if no active side-effect function
// Level 1: Find or create the depsMap corresponding to the target
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// Level 2: Find or create the dep set corresponding to the key
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// Level 3: Add the current active side-effect function to the dep
trackEffects(dep)
}
function trackEffects(dep) {
if (dep.has(activeEffect)) return // Avoid duplicate collection
dep.add(activeEffect)
// Simultaneously, the side-effect function also needs to record which deps it belongs to (for cleanup)
activeEffect.deps.push(dep)
}
Step 2: Proxy Getter Interception
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// Dependency tracking
track(target, key)
if (isObject(res)) {
return reactive(res) // Deep reactivity
}
return res
}
}
4. Update Triggering (Trigger) Process
Step 1: Find Dependencies and Execute
function trigger(target, key) {
// Find the depsMap corresponding to the target from targetMap
const depsMap = targetMap.get(target)
if (!depsMap) return // Return directly if no dependencies
// Find the dep set corresponding to the key
const dep = depsMap.get(key)
if (dep) {
triggerEffects(dep)
}
}
function triggerEffects(dep) {
// Avoid infinite loops caused by triggering collection while executing effects
const effects = new Set(dep)
effects.forEach(effect => {
// If the effect has a scheduler, execute via the scheduler (for optimizations like batch updates)
if (effect.scheduler) {
effect.scheduler()
} else {
effect() // Directly execute the side-effect function
}
})
}
Step 2: Proxy Setter Interception
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
// Trigger update (only trigger when the value actually changes)
if (hasChanged(value, oldValue)) {
trigger(target, key)
}
return result
}
}
5. Complete Reactive Function Example
let activeEffect = null
class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn
this.scheduler = scheduler
this.deps = [] // Record which deps have collected this effect
}
run() {
activeEffect = this
const result = this.fn()
activeEffect = null
return result
}
}
function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
// Return a runner function that can be manually executed
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
6. Dependency Cleanup Mechanism
Why Cleanup is Needed:
- Side-effect functions may no longer depend on certain data and need to be removed from the corresponding dep
- Avoid memory leaks and unnecessary updates
Cleanup Implementation:
class ReactiveEffect {
constructor(fn, scheduler) {
// ...other code
this.deps = []
}
run() {
// First, clean up previous dependencies
cleanupEffect(this)
activeEffect = this
const result = this.fn()
activeEffect = null
return result
}
}
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect) // Remove this effect from all deps
}
deps.length = 0 // Clear the deps array
}
}
7. Practical Application Example
const obj = reactive({ count: 0, name: 'vue' })
// Side-effect function 1: Only depends on count
effect(() => {
console.log('count changed:', obj.count)
})
// Side-effect function 2: Depends on both count and name
effect(() => {
console.log('both changed:', obj.count, obj.name)
})
obj.count++ // Triggers both effects
obj.name = 'vue3' // Only triggers the second effect
Summary
Vue3's dependency tracking and update triggering mechanism establishes precise dependency relationships through a three-level mapping structure. It collects dependencies during get operations and triggers updates during set operations. By tracking the currently active side-effect function via effect, combined with the data structures WeakMap, Map, and Set, it achieves efficient reactive updates. The cleanup mechanism ensures the accuracy of dependency relationships and avoids memory leaks.