Skip to content

第 5 章 @vue/reactivity 源码深度剖析(下):effect / effectScope / shallowReactive / readonly

本章要点

  • effect() 的完整生命周期:创建、执行、依赖收集、清理、销毁
  • effectScope:为什么需要"作用域"来管理副作用
  • shallowReactive / shallowRef:何时需要"浅层"响应式
  • readonly / shallowReadonly:编译器如何利用只读优化
  • 响应式工具函数全景:toRaw / markRaw / isRef / unref / toRefs

你是否遇到过这样的场景:一个页面打开了 WebSocket 连接、启动了定时器、注册了 watchEffect——当用户离开页面时,这些副作用都需要被清理。忘记清理任何一个,都会导致内存泄漏。

在 Vue 2 中,你需要在 beforeUnmount 中手动管理每一个副作用的清理。在 Vue 3 中,effectScope 让你可以一行代码批量清理所有副作用。

effectScope 是如何做到的?它和 effect 之间的关系是什么?effect 本身的生命周期又是怎样的?

本章将继续深入 @vue/reactivity,完成响应式系统的最后几块拼图。

5.1 effect():响应式系统的执行引擎

effect 的本质

effect 是响应式系统中"副作用"的载体。当你写 watchEffectwatch,或者 Vue 内部创建组件的渲染更新函数时,底层都是 effect

typescript
import { ref, effect } from '@vue/reactivity'

const count = ref(0)

// 创建一个 effect
const runner = effect(() => {
  console.log(`count is: ${count.value}`)
})
// 输出: count is: 0

count.value = 1
// 输出: count is: 1(自动重新执行)

count.value = 2
// 输出: count is: 2(再次自动重新执行)

ReactiveEffect 类

typescript
// packages/reactivity/src/effect.ts(简化)

export class ReactiveEffect<T = any> implements Subscriber {
  // --- Subscriber 接口 ---
  _deps: Link | undefined = undefined
  _depsTail: Link | undefined = undefined
  _flags: number = SubscriberFlags.ACTIVE

  // --- Effect 特有 ---
  _fn: () => T
  _scheduler: EffectScheduler | undefined
  _cleanup: (() => void) | undefined

  // 当前作用域
  _scope: EffectScope | undefined

  constructor(fn: () => T) {
    this._fn = fn
    // 自动注册到当前活跃的 EffectScope
    if (activeEffectScope) {
      this._scope = activeEffectScope
      activeEffectScope.effects.push(this)
    }
  }

  run(): T {
    // 1. 如果已停止,直接执行函数(不收集依赖)
    if (!(this._flags & SubscriberFlags.ACTIVE)) {
      return this._fn()
    }

    // 2. 设置当前活跃 subscriber
    const prevSub = activeSub
    activeSub = this

    // 3. 开启追踪
    const prevShouldTrack = shouldTrack
    shouldTrack = true

    try {
      // 4. 准备清理旧依赖
      prepareDeps(this)

      // 5. 执行函数 — 触发 getter → track → 建立新依赖
      const result = this._fn()

      // 6. 清理不再需要的旧依赖
      cleanupDeps(this)

      return result
    } finally {
      // 7. 恢复之前的上下文
      activeSub = prevSub
      shouldTrack = prevShouldTrack
    }
  }

  stop(): void {
    if (this._flags & SubscriberFlags.ACTIVE) {
      // 清理所有依赖链接
      removeDeps(this)
      // 执行清理回调
      if (this._cleanup) {
        this._cleanup()
      }
      this._flags &= ~SubscriberFlags.ACTIVE
    }
  }
}

依赖的生命周期管理

每次 effect.run() 执行时,需要处理一个微妙的问题:依赖可能在两次执行之间发生变化

考虑这个场景:

typescript
const show = ref(true)
const a = ref('hello')
const b = ref('world')

effect(() => {
  if (show.value) {
    console.log(a.value)  // 第一次执行:依赖 show 和 a
  } else {
    console.log(b.value)  // 第二次执行:依赖 show 和 b
  }
})

show.valuetrue 变为 false 时,effect 重新执行。此时它不再依赖 a,而是依赖 b。旧的对 a 的依赖必须被清理,否则 a.value 变化时仍会触发这个 effect——这就是"过期依赖"问题。

typescript
// packages/reactivity/src/dep.ts(简化)

function prepareDeps(sub: Subscriber): void {
  // 遍历所有旧的依赖 Link,标记版本号为 -1
  let link = sub._deps
  while (link) {
    link.version = -1  // ← 标记为"待验证"
    link = link.nextDep
  }
}

function cleanupDeps(sub: Subscriber): void {
  // 遍历所有 Link,移除版本号仍为 -1 的(未被重新访问的依赖)
  let link = sub._deps
  let prev: Link | undefined

  while (link) {
    const next = link.nextDep
    if (link.version === -1) {
      // 这个依赖在最近一次执行中没有被访问 → 移除
      unlinkDep(link)
      if (prev) {
        prev.nextDep = next
      } else {
        sub._deps = next
      }
    } else {
      prev = link
    }
    link = next
  }
  sub._depsTail = prev
}

🔥 深度洞察

prepareDeps + cleanupDeps 的"标记-清扫"策略,与 GC(垃圾回收)的标记-清扫算法如出一辙。在 Vue 3.0–3.4 中,处理过期依赖的方式是"先全部删除,再重新收集"——每次执行都从零开始。Alien Signals 的方式更聪明:先标记所有旧依赖为"待验证"(version = -1),执行过程中重新访问的依赖会被更新为新版本号。执行结束后,仍然是 -1 的就是过期依赖——只移除这些。这样,稳定的依赖(每次执行都被访问的)不需要任何删除-重建操作,只有真正变化的依赖才产生开销。

onCleanup——副作用的清理

Vue 3.5 引入了 onCleanup 回调,让 effect 可以在每次重新执行前执行清理逻辑:

typescript
import { ref, watchEffect } from 'vue'

const id = ref(1)

watchEffect((onCleanup) => {
  const controller = new AbortController()

  fetch(`/api/data/${id.value}`, { signal: controller.signal })
    .then(res => res.json())
    .then(data => { /* 使用数据 */ })

  // 当 id 变化导致 effect 重新执行时,取消上一次的请求
  onCleanup(() => {
    controller.abort()
  })
})

onCleanup 的实现原理很简单——它注册一个清理函数到 effect._cleanup,在下一次 effect.run() 之前被调用:

typescript
// packages/reactivity/src/effect.ts(简化)

function run() {
  // 在执行新函数之前,调用上一次注册的清理函数
  if (this._cleanup) {
    this._cleanup()
    this._cleanup = undefined
  }
  // ... 执行 this._fn()
}

5.2 effectScope:批量管理副作用

问题场景

在复杂的组合式函数中,你可能创建多个 watchwatchEffectcomputed

typescript
function useFeature() {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  const formatted = computed(() => /* ... */)

  watchEffect(() => { /* 自动请求数据 */ })
  watch(data, () => { /* 数据变化时的副作用 */ })

  // 如何一次性清理所有这些副作用?
}

在没有 effectScope 的情况下,你需要手动收集每个副作用的停止句柄:

typescript
// 麻烦的手动管理
function useFeature() {
  const stops: (() => void)[] = []

  const data = ref(null)
  stops.push(watchEffect(() => { /* ... */ }))
  stops.push(watch(data, () => { /* ... */ }))

  function cleanup() {
    stops.forEach(stop => stop())
  }

  return { data, cleanup }
}

effectScope 的解决方案更加优雅:

typescript
import { effectScope } from 'vue'

function useFeature() {
  const scope = effectScope()

  scope.run(() => {
    const data = ref(null)
    watchEffect(() => { /* ... */ })
    watch(data, () => { /* ... */ })
    // 在 scope.run() 内创建的所有 effect 都被自动收集
  })

  // 一行代码停止所有副作用
  scope.stop()
}

EffectScope 的实现

typescript
// packages/reactivity/src/effectScope.ts(简化)

export let activeEffectScope: EffectScope | undefined

export class EffectScope {
  _active = true
  effects: ReactiveEffect[] = []
  cleanups: (() => void)[] = []

  // 子作用域(树形结构)
  scopes: EffectScope[] | undefined
  parent: EffectScope | undefined

  constructor(detached = false) {
    // 如果不是"分离的",自动注册到父作用域
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      ;(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(this)
    }
  }

  run<T>(fn: () => T): T | undefined {
    if (this._active) {
      const prevScope = activeEffectScope
      activeEffectScope = this  // ← 设为当前活跃作用域
      try {
        return fn()            // ← fn 内创建的 effect 自动注册到 this
      } finally {
        activeEffectScope = prevScope
      }
    }
  }

  stop(fromParent?: boolean): void {
    if (this._active) {
      // 停止所有收集到的 effect
      for (const effect of this.effects) {
        effect.stop()
      }
      // 执行所有清理回调
      for (const cleanup of this.cleanups) {
        cleanup()
      }
      // 递归停止子作用域
      if (this.scopes) {
        for (const scope of this.scopes) {
          scope.stop(true)
        }
      }
      // 从父作用域中移除自己
      if (!fromParent && this.parent) {
        const i = this.parent.scopes!.indexOf(this)
        if (i > -1) this.parent.scopes!.splice(i, 1)
      }
      this._active = false
    }
  }
}

基于 VitePress 构建