阿里云主机折上折
  • 微信号
Current Site:Index > The detailed process of dependency collection and tracking

The detailed process of dependency collection and tracking

Author:Chuan Chen 阅读数:49772人阅读 分类: Vue.js

Overview of Dependency Collection and Tracking Process

The core of Vue 3's reactivity system lies in dependency collection and tracking. When data changes, it can automatically notify the side effects that depend on it to re-execute. This process is achieved by intercepting get/set operations through Proxy, combined with the track and trigger functions.

Creation of Reactive Objects

When creating a reactive object through the reactive() function, the internal createReactiveObject is called:

function reactive(target) {
  return createReactiveObject(
    target,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

function createReactiveObject(target, baseHandlers) {
  const proxy = new Proxy(target, baseHandlers)
  return proxy
}

baseHandlers contains the get/set traps for Proxy:

const mutableHandlers = {
  get: createGetter(),
  set: createSetter()
}

Dependency Collection in Getter

When accessing a reactive property, the get trap is triggered:

function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    track(target, key) // Key dependency collection
    if (isObject(res)) {
      return reactive(res) // Deep reactivity
    }
    return res
  }
}

The track function establishes the relationship between the property and the currently running side effect:

const targetMap = new WeakMap() // Global dependency storage

function track(target, key) {
  if (!activeEffect) return // No active effect means no tracking
  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) // Add the current effect to the dependency set
  activeEffect.deps.push(dep) // Reverse record
}

Registration and Execution of Side Effects

Side effects are registered through the effect function:

let activeEffect

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn) // Clear old dependencies
    activeEffect = effectFn
    fn()
  }
  effectFn.deps = []
  effectFn()
}

Clearing old dependencies to avoid invalid updates:

function cleanup(effectFn) {
  for (const dep of effectFn.deps) {
    dep.delete(effectFn)
  }
  effectFn.deps.length = 0
}

Dependency Triggering in Setter

Modifying a property value triggers the set trap:

function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if (hasChanged(value, oldValue)) {
      trigger(target, key) // Trigger updates
    }
    return result
  }
}

trigger looks up and executes related side effects:

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  const effectsToRun = new Set(effects)
  effectsToRun.forEach(effect => effect())
}

Handling Nested Effects

Component rendering may produce nested effects:

effect(() => {
  console.log('Outer effect')
  effect(() => {
    console.log('Inner effect')
    temp2 = obj.bar // Collect inner dependencies
  })
  temp1 = obj.foo // Collect outer dependencies
})

Maintaining execution context through effectStack:

const effectStack = []

function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    activeEffect = effectFn
    effectStack.push(effectFn)
    fn()
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  effectFn.deps = []
  effectFn()
}

Special Handling for Array Methods

Array methods like push/pop require additional handling:

const arrayInstrumentations = {
  push() {
    track(this, 'length') // Track length changes
    return Array.prototype.push.apply(this, arguments)
  }
}

function createGetter() {
  return function get(target, key, receiver) {
    if (isArray(target) && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }
    // ...Original logic
  }
}

Scheduling Execution Control

Controlling trigger timing through scheduler:

function trigger(target, key) {
  // ...
  const run = (effect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }
  effectsToRun.forEach(run)
}

Example usage:

effect(() => {
  console.log(obj.foo)
}, {
  scheduler(effect) {
    setTimeout(effect, 1000) // Delay execution
  }
})

Implementation of Computed Properties

Computed properties are based on lazy execution of effects:

function computed(getter) {
  let value
  let dirty = true
  
  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      dirty = true
      trigger(obj, 'value') // Manually trigger dependencies
    }
  })
  
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      track(obj, 'value') // Manually collect dependencies
      return value
    }
  }
  
  return obj
}

Implementation Principle of Watch

watch is based on effects and schedulers:

function watch(source, cb) {
  let getter
  if (isFunction(source)) {
    getter = source
  } else {
    getter = () => traverse(source)
  }
  
  let oldValue
  const effectFn = effect(
    () => getter(),
    {
      scheduler() {
        const newValue = effectFn()
        cb(newValue, oldValue)
        oldValue = newValue
      }
    }
  )
  
  oldValue = effectFn()
}

Recursively reading properties to ensure full tracking:

function traverse(value, seen = new Set()) {
  if (!isObject(value) || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

Performance Optimization of the Reactivity System

  1. Dependency collection level optimization:
function track(target, key) {
  if (!shouldTrack) return // Global switch
  if (key === '__proto__' || key === 'constructor') return
  // ...Remaining logic
}
  1. Avoiding duplicate triggers:
function set(target, key, value) {
  if (Array.isArray(target) && key === 'length') {
    if (value >= target.length) return // Ignore length increase
  }
  // ...Original logic
}
  1. Batch update mechanism:
let isFlushing = false
const queue = new Set()

function queueJob(job) {
  queue.add(job)
  if (!isFlushing) {
    isFlushing = true
    Promise.resolve().then(() => {
      try {
        queue.forEach(job => job())
      } finally {
        isFlushing = false
        queue.clear()
      }
    })
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.