Effect scope API
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
- A scope can only be stopped once
- A stopped scope cannot run new side effects
- Stopping nested scopes is cascading
- 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
- Create separate scopes for each logical unit
- Return a stop method in composable functions
- Give scopes meaningful names for debugging
- Avoid running new side effects in stopped scopes
- Consider using
onScopeDispose
instead of directonUnmounted
import { onScopeDispose } from 'vue'
const scope = effectScope()
scope.run(() => {
onScopeDispose(() => {
// Scope-specific cleanup logic
})
})
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn