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——这就是"过期依赖"问题。

基于 VitePress 构建