阿里云主机折上折
  • 微信号
Current Site:Index > Effect scope API

Effect scope API

Author:Chuan Chen 阅读数:15026人阅读 分类: Vue.js

Introduction to Effect Scope API

Vue 3's Effect Scope API provides a way to organize and manage side effects. It allows developers to create independent effect scopes and manually stop or pause these side effects when needed. This API is particularly useful for cleaning up side effects when components are unmounted or when precise control over the execution timing of side effects is required.

Basic Usage

Creating an effect scope is straightforward using the effectScope() function:

import { effectScope } from 'vue'

const scope = effectScope()

Multiple side effects can be run within this scope:

scope.run(() => {
  const state = reactive({ count: 0 })
  
  watchEffect(() => {
    console.log(state.count)
  })
  
  watch(() => state.count, (newVal) => {
    console.log('Count changed:', newVal)
  })
})

Stopping a Scope

When the side effects within a scope are no longer needed, you can stop all of them by calling the stop() method:

scope.stop()

This is equivalent to manually calling each side effect's cleanup function. It is especially useful when a component is unmounted:

import { onUnmounted } from 'vue'

onUnmounted(() => {
  scope.stop()
})

Nested Scopes

Effect scopes can be nested, with child scopes inheriting behavior from their parent:

const parentScope = effectScope()

parentScope.run(() => {
  const childScope = effectScope()
  
  childScope.run(() => {
    // Side effects here belong to the childScope
  })
  
  // Stopping the parent scope also stops the child scope
  parentScope.stop()
})

Automatic Dependency Collection

Reactive dependencies created within a scope are automatically collected:

const scope = effectScope()

scope.run(() => {
  const state = reactive({ 
    name: 'Vue',
    version: 3
  })
  
  // This computed property will be automatically collected
  const info = computed(() => `${state.name} ${state.version}`)
  
  watchEffect(() => {
    console.log(info.value)
  })
})

Practical Use Cases

Usage in Components

Using effect scopes in components allows for better side effect management:

import { effectScope, onUnmounted } from 'vue'

export default {
  setup() {
    const scope = effectScope()
    
    scope.run(() => {
      // All side effects in setup
      const state = reactive({ /* ... */ })
      watchEffect(/* ... */)
      // ...
    })
    
    onUnmounted(() => scope.stop())
    
    return { /* ... */ }
  }
}

Composable Functions

Using effect scopes in composable functions:

function useFeature() {
  const scope = effectScope()
  const state = reactive({ /* ... */ })
  
  scope.run(() => {
    watchEffect(/* ... */)
    // Other side effects
  })
  
  return {
    ...toRefs(state),
    stop: () => scope.stop()
  }
}

Advanced Usage

Isolating Scopes

Certain side effects can be isolated into separate scopes:

const mainScope = effectScope()
const analyticsScope = effectScope()

mainScope.run(() => {
  // Main logic side effects
})

analyticsScope.run(() => {
  // Analytics-related side effects
})

// Analytics-related side effects can be stopped separately
analyticsScope.stop()

Scope State

You can check the current state of a scope:

const scope = effectScope()

console.log(scope.active) // true

scope.stop()

console.log(scope.active) // false

Integration with Suspense

Effect scopes can be used with Suspense:

const scope = effectScope()

async function setup() {
  scope.run(() => {
    // Async side effects
  })
  
  return scope
}

// Inside a Suspense boundary
const result = await setup()
// When Suspense resolves
result.stop()

Performance Considerations

Effect scopes can help optimize performance:

// Create a scope when needed
const heavyComputationScope = effectScope()

function startHeavyComputation() {
  heavyComputationScope.run(() => {
    // Perform computation-intensive operations
  })
}

function stopHeavyComputation() {
  heavyComputationScope.stop()
}

Integration with Pinia

Using effect scopes with the Pinia state management library:

import { defineStore } from 'pinia'

export const useStore = defineStore('main', () => {
  const scope = effectScope()
  const state = reactive({ /* ... */ })
  
  scope.run(() => {
    // Store side effects
    watchEffect(/* ... */)
  })
  
  return {
    ...toRefs(state),
    stop: () => scope.stop()
  }
})

Debugging Tips

You can name scopes for easier debugging:

const scope = effectScope('ComponentScope')

// The name will appear in devtools

Limitations and Notes

  1. A scope can only be stopped once
  2. A stopped scope cannot run new side effects
  3. Stopping nested scopes is cascading
  4. Pay special attention to scope lifecycle in SSR environments

Comparison with Other APIs

Compared to directly using watch and watchEffect, effect scopes provide a more centralized management approach:

// Traditional approach
const stop1 = watchEffect(/* ... */)
const stop2 = watch(/* ... */)
// Need to manually stop each
stop1()
stop2()

// Using scopes
const scope = effectScope()
scope.run(() => {
  watchEffect(/* ... */)
  watch(/* ... */)
})
// Stop all at once
scope.stop()

Source Code Analysis

The implementation of effect scopes is primarily based on Vue's reactivity system:

class EffectScope {
  constructor(detached = false) {
    this.active = true
    this.effects = []
    this.cleanups = []
    // ...
  }
  
  run(fn) {
    if (this.active) {
      try {
        return fn()
      } finally {
        // Cleanup
      }
    }
  }
  
  stop() {
    if (this.active) {
      this.active = false
      this.effects.forEach(effect => effect.stop())
      this.cleanups.forEach(cleanup => cleanup())
      // ...
    }
  }
}

Testing Strategies

When testing effect scopes, consider the following aspects:

import { effectScope, watchEffect } from 'vue'

test('effect scope basic usage', () => {
  const scope = effectScope()
  let count = 0
  
  scope.run(() => {
    watchEffect(() => {
      count++
    })
  })
  
  expect(count).toBe(1)
  scope.stop()
  // Verify side effects are stopped
})

Migration Guide

When migrating from Vue 2 to Vue 3, you can refactor existing code using effect scopes:

// Vue 2 approach
export default {
  created() {
    this._watchers = []
    this._watchers.push(this.$watch(/* ... */))
  },
  beforeDestroy() {
    this._watchers.forEach(unwatch => unwatch())
  }
}

// Vue 3 approach
export default {
  setup() {
    const scope = effectScope()
    
    scope.run(() => {
      watch(/* ... */)
      // Other side effects
    })
    
    onUnmounted(() => scope.stop())
  }
}

Common Issue Resolution

Scope Not Properly Stopped

Ensure the scope is stopped when the component is unmounted:

// Wrong approach - potential memory leak
const scope = effectScope()
scope.run(/* ... */)

// Correct approach
const scope = effectScope()
onUnmounted(() => scope.stop())
scope.run(/* ... */)

Handling Async Side Effects

Be cautious with async side effects:

const scope = effectScope()

async function loadData() {
  const data = await fetchData()
  
  // Ensure the scope is still active
  if (scope.active) {
    scope.run(() => {
      // Side effects using data
    })
  }
}

loadData()

Best Practice Recommendations

  1. Create separate scopes for each logical unit
  2. Return a stop method in composable functions
  3. Give scopes meaningful names for debugging
  4. Avoid running new side effects in stopped scopes
  5. Consider using onScopeDispose instead of direct onUnmounted
import { onScopeDispose } from 'vue'

const scope = effectScope()

scope.run(() => {
  onScopeDispose(() => {
    // Scope-specific cleanup logic
  })
})

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.