Skip to content

在 Vue 3 的响应式系统中,effectScope 是一个相对高级但非常实用的 API,它允许开发者更精细地控制响应式副作用(effects)的生命周期。通过 effectScope,我们可以将一组相关的副作用组织在一起,并能够一次性停止它们,这对于构建复杂的响应式系统和插件非常重要。

一、effectScope 的基本概念与使用

effectScope 是 Vue 3.2 引入的新特性,它提供了一种机制来收集和管理响应式副作用,并能够在需要的时候统一停止这些副作用。

js
import { effectScope, ref, watchEffect } from 'vue'

// 创建一个 effect 作用域
const scope = effectScope()

// 在作用域内运行副作用
scope.run(() => {
  const count = ref(0)
  
  watchEffect(() => {
    console.log(`Count: ${count.value}`)
  })
  
  setInterval(() => {
    count.value++
  }, 1000)
})

// 在适当的时候停止所有副作用
scope.stop()

在上面的例子中,我们创建了一个 effect 作用域,并在其中运行了一些副作用。当我们调用 scope.stop() 时,所有在该作用域内创建的副作用都会被停止,包括 和定时器。

二、effectScope 的工作机制

effectScope 的实现基于 Vue 的响应式系统内部机制。每个 effectScope 实例本质上是一个副作用收集器,它会跟踪在其内部创建的所有副作用。

js
// 简化的 effectScope 实现
class EffectScope {
  constructor(detached = false) {
    this.detached = detached // 是否独立于父作用域
    this.active = true       // 是否处于活跃状态
    this.effects = []        // 收集的副作用
    this.cleanups = []       // 清理函数
    this._isPaused = false   // 是否暂停
    this.parent = getCurrentScope() // 父作用域
    
    // 如果不是独立的且有父作用域,则加入父作用域
    if (!detached && this.parent) {
      this.parent.scopes = this.parent.scopes || []
      this.parent.scopes.push(this)
    }
  }
  
  // 在作用域内运行函数
  run(fn) {
    if (this.active) {
      const currentScope = activeEffectScope
      try {
        activeEffectScope = this
        return fn()
      } finally {
        activeEffectScope = currentScope
      }
    }
  }
  
  // 停止作用域内的所有副作用
  stop() {
    if (this.active) {
      // 停止所有副作用
      for (let i = 0; i < this.effects.length; i++) {
        this.effects[i].stop()
      }
      this.effects.length = 0
      
      // 执行所有清理函数
      for (let i = 0; i < this.cleanups.length; i++) {
        this.cleanups[i]()
      }
      this.cleanups.length = 0
      
      // 停止所有子作用域
      if (this.scopes) {
        for (let i = 0; i < this.scopes.length; i++) {
          this.scopes[i].stop()
        }
        this.scopes.length = 0
      }
      
      this.active = false
    }
  }
  
  // 注册清理函数
  on() {
    // 增加引用计数
    this._on++
    if (this._on === 1) {
      this.prevScope = activeEffectScope
      activeEffectScope = this
    }
  }
  
  // 取消注册清理函数
  off() {
    if (this._on > 0) {
      this._on--
      if (this._on === 0) {
        activeEffectScope = this.prevScope
        this.prevScope = undefined
      }
    }
  }
}

三、effectScope 与组件生命周期

在 Vue 组件中,每个组件实例都会创建自己的 effectScope,用于收集该组件的所有副作用(如 计算属性等)。当组件被卸载时,其对应的 effectScope 会被自动停止,从而清理所有副作用。

js
// 组件实例的简化结构
const instance = {
  scope: effectScope(),
  
  // 组件挂载
  mount() {
    this.scope.run(() => {
      // 在组件作用域内创建副作用
      setupComponent(this)
    })
  },
  
  // 组件卸载
  unmount() {
    // 停止组件作用域,清理所有副作用
    this.scope.stop()
  }
}

这种方式确保了组件的副作用能够随着组件的生命周期自动管理,避免了内存泄漏。

四、独立作用域与嵌套作用域

effectScope 支持创建独立作用域和嵌套作用域:

js
// 创建独立作用域(不与父作用域关联)
const detachedScope = effectScope(true)

detachedScope.run(() => {
  // 在独立作用域内创建副作用
  const count = ref(0)
  
  watchEffect(() => {
    console.log(count.value)
  })
})

// 即使父作用域停止,独立作用域仍然有效
detachedScope.stop() // 手动停止独立作用域

// 嵌套作用域
const parentScope = effectScope()
parentScope.run(() => {
  const childScope = effectScope()
  childScope.run(() => {
    // 子作用域中的副作用
    watchEffect(() => {
      // ...
    })
  })
  
  // 停止父作用域时,子作用域也会被停止
})

五、effectScope 在实际应用中的使用场景

1. 插件开发

在开发 Vue 插件时,effectScope 可以帮助管理插件创建的副作用:

js
// 简化的插件示例
function createPlugin() {
  const scope = effectScope()
  
  return {
    install(app) {
      scope.run(() => {
        // 插件的副作用逻辑
        const globalState = ref({})
        
        app.provide('globalState', globalState)
        
        watchEffect(() => {
          // 监听全局状态变化
          console.log('Global state changed:', globalState.value)
        })
      })
    },
    
    // 提供卸载方法
    uninstall() {
      scope.stop()
    }
  }
}

2. 可组合函数(Composables)

在编写可组合函数时,effectScope 可以帮助管理副作用的生命周期:

js
import { effectScope, ref, watch } from 'vue'

// 创建一个可组合函数
export function useMouseTracker() {
  const x = ref(0)
  const y = ref(0)
  
  const scope = effectScope()
  
  scope.run(() => {
    const update = (e) => {
      x.value = e.pageX
      y.value = e.pageY
    }
    
    window.addEventListener('mousemove', update)
    
    // 注册清理函数
    scope.cleanups.push(() => {
      window.removeEventListener('mousemove', update)
    })
  })
  
  return {
    x,
    y,
    stop: () => scope.stop()
  }
}

3. 复杂的状态管理

在复杂的状态管理场景中,effectScope 可以帮助组织和管理相关的副作用:

js
class StateManager {
  constructor() {
    this.scope = effectScope()
    this.state = reactive({})
  }
  
  initState(config) {
    this.scope.run(() => {
      // 根据配置创建相关的副作用
      Object.keys(config).forEach(key => {
        const source = config[key]
        
        if (source.watch) {
          watch(source.ref, (newValue) => {
            this.state[key] = newValue
          })
        }
      })
    })
  }
  
  destroy() {
    // 一次性清理所有副作用
    this.scope.stop()
  }
}

六、effectScope 与其他 API 的关系

effectScope 与 Vue 的其他响应式 API 紧密配合:

  1. 与 配合:所有通过这些 API 创建的副作用都会被自动收集到当前的 effectScope 中。

  2. 与 getCurrentScope 配合:可以获取当前活跃的 effectScope 实例。

  3. 与 onScopeDispose 配合:可以在 effectScope 中注册清理函数。

js
import { effectScope, onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
  // 注册清理函数
  onScopeDispose(() => {
    console.log('Scope is stopping')
  })
})

scope.stop() // 输出: "Scope is stopping"

七、总结

effectScope 是 Vue 3 响应式系统中一个强大的机制,它提供了一种精细化管理副作用生命周期的方式:

  • 作用域管理:通过 effectScope 可以将相关的副作用组织在一起
  • 生命周期控制:通过 scope.stop() 可以一次性停止作用域内的所有副作用
  • 内存管理:合理的使用 effectScope 可以避免内存泄漏
  • 插件友好:为插件和可组合函数提供了更好的副作用管理方式

effectScope 的引入完善了 Vue 的响应式系统,使得开发者能够更精确地控制副作用的创建和销毁,特别是在构建复杂应用、插件或可组合函数时,它是一个非常有价值的工具。