Vue3 的响应式系统源码级副作用清理(cleanup)与嵌套 effect 处理原理
字数 991 2025-11-16 03:15:12

Vue3 的响应式系统源码级副作用清理(cleanup)与嵌套 effect 处理原理

1. 问题描述

在 Vue3 的响应式系统中,副作用函数(effect)可能会依赖多个响应式数据,当依赖变化时需重新执行 effect。但若依赖关系动态变化(如条件分支中依赖不同数据),需清理无效依赖以避免冗余更新。同时,effect 可能嵌套(如组件渲染嵌套),需正确建立依赖关系与清理机制。


2. 核心概念:副作用函数与依赖收集

  • 副作用函数(effect):封装响应式数据变化的执行逻辑(如组件渲染、计算属性)。
  • 依赖收集:通过 Proxy 的 getter 拦截,将当前执行的 effect 存入响应式数据的依赖集合(Set)中。
  • 触发更新:数据修改时,从依赖集合中取出所有 effect 重新执行。

3. 为什么需要清理依赖?

场景示例

const state = reactive({ flag: true, a: 1, b: 2 })  
effect(() => {  
  if (state.flag) {  
    console.log(state.a) // 依赖 a  
  } else {  
    console.log(state.b) // 依赖 b  
  }  
})  

初始时 flag=true,effect 依赖 flaga。若将 flag 改为 false,effect 应依赖 flagb,但旧依赖 a 仍保留。若修改 a,会错误触发 effect 重新执行。


4. 源码级清理机制实现

(1)effect 的结构设计

let activeEffect // 当前正在执行的 effect  
class ReactiveEffect {  
  constructor(fn) {  
    this.fn = fn  
    this.deps = [] // 存储所有包含本 effect 的依赖集合(Set)  
  }  
  run() {  
    activeEffect = this  
    cleanupEffect(this) // 执行前清理旧依赖  
    return this.fn()  
  }  
}  

(2)清理函数 cleanupEffect

function cleanupEffect(effect) {  
  const { deps } = effect  
  for (const dep of deps) {  
    dep.delete(effect) // 从所有依赖集合中移除本 effect  
  }  
  effect.deps.length = 0 // 清空 deps 数组  
}  

(3)依赖收集的双向关联

在 Proxy getter 中调用 track 函数:

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)  
  activeEffect.deps.push(dep) // 将依赖集合记录到 effect.deps  
}  

关键:每个 effect 通过 deps 记录自己属于哪些依赖集合(Set),方便后续清理。


5. 嵌套 effect 的处理原理

场景示例(组件嵌套)

effect(() => {  
  console.log('父效应')  
  effect(() => {  
    console.log('子效应')  
  })  
})  

解决方案:effectStack

let effectStack = [] // 栈结构管理嵌套 effect  
class ReactiveEffect {  
  run() {  
    if (effectStack.includes(this)) return // 避免无限递归  
    try {  
      effectStack.push(this)  
      activeEffect = this  
      cleanupEffect(this)  
      return this.fn()  
    } finally {  
      effectStack.pop() // 执行完成后出栈  
      activeEffect = effectStack[effectStack.length - 1] // 恢复父 effect  
    }  
  }  
}  

流程

  1. 执行父 effect 前,将其入栈并设为 activeEffect
  2. 子 effect 执行时,父 effect 暂存于栈中,子 effect 成为新的 activeEffect
  3. 子 effect 执行完毕出栈,恢复父 effect 为 activeEffect

6. 综合流程示例

const state = reactive({ flag: true, a: 1, b: 2 })  
effect(() => {  
  console.log('effect运行')  
  if (state.flag) {  
    console.log('依赖a:', state.a)  
  } else {  
    console.log('依赖b:', state.b)  
  }  
})  

// 初始:effect 依赖 flag 和 a  
state.flag = false // 触发 effect 重新运行  
// 重新收集依赖:清理旧依赖(a)后,新依赖为 flag 和 b  
state.a = 100 // 不会触发 effect(因已清理 a 的依赖)  

7. 总结

  • 依赖清理:通过 effect.deps 记录所有依赖集合,每次执行 effect 前清理旧依赖,避免无效更新。
  • 嵌套处理:利用栈结构管理嵌套 effect,确保依赖收集指向正确的 activeEffect
  • 性能优化:避免冗余依赖导致不必要的重渲染,提升响应式系统精确性。
Vue3 的响应式系统源码级副作用清理(cleanup)与嵌套 effect 处理原理 1. 问题描述 在 Vue3 的响应式系统中,副作用函数(effect)可能会依赖多个响应式数据,当依赖变化时需重新执行 effect。但若依赖关系动态变化(如条件分支中依赖不同数据),需清理无效依赖以避免冗余更新。同时,effect 可能嵌套(如组件渲染嵌套),需正确建立依赖关系与清理机制。 2. 核心概念:副作用函数与依赖收集 副作用函数(effect) :封装响应式数据变化的执行逻辑(如组件渲染、计算属性)。 依赖收集 :通过 Proxy 的 getter 拦截,将当前执行的 effect 存入响应式数据的依赖集合(Set)中。 触发更新 :数据修改时,从依赖集合中取出所有 effect 重新执行。 3. 为什么需要清理依赖? 场景示例 初始时 flag=true ,effect 依赖 flag 和 a 。若将 flag 改为 false ,effect 应依赖 flag 和 b ,但旧依赖 a 仍保留。若修改 a ,会错误触发 effect 重新执行。 4. 源码级清理机制实现 (1)effect 的结构设计 (2)清理函数 cleanupEffect (3)依赖收集的双向关联 在 Proxy getter 中调用 track 函数: 关键 :每个 effect 通过 deps 记录自己属于哪些依赖集合(Set),方便后续清理。 5. 嵌套 effect 的处理原理 场景示例(组件嵌套) 解决方案:effectStack 流程 : 执行父 effect 前,将其入栈并设为 activeEffect 。 子 effect 执行时,父 effect 暂存于栈中,子 effect 成为新的 activeEffect 。 子 effect 执行完毕出栈,恢复父 effect 为 activeEffect 。 6. 综合流程示例 7. 总结 依赖清理 :通过 effect.deps 记录所有依赖集合,每次执行 effect 前清理旧依赖,避免无效更新。 嵌套处理 :利用栈结构管理嵌套 effect,确保依赖收集指向正确的 activeEffect 。 性能优化 :避免冗余依赖导致不必要的重渲染,提升响应式系统精确性。