The detailed process of dependency collection and tracking
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
- Dependency collection level optimization:
function track(target, key) {
if (!shouldTrack) return // Global switch
if (key === '__proto__' || key === 'constructor') return
// ...Remaining logic
}
- Avoiding duplicate triggers:
function set(target, key, value) {
if (Array.isArray(target) && key === 'length') {
if (value >= target.length) return // Ignore length increase
}
// ...Original logic
}
- 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
下一篇:effect函数的内部工作原理