Effect Cleanup and Effect Scope
WARNING
Vue 3.4 was released at the end of December 2023, which includes performance improvements for reactivity.
You should note that this online book is referencing the previous implementation.
We plan to update this online book at the appropriate time.
Cleanup of ReactiveEffect
We haven't been cleaning up the effects we registered so far. Let's add cleanup processing to ReactiveEffect.
Implement a method called stop
in ReactiveEffect.
Add a flag to ReactiveEffect to indicate whether it is active or not, and in the stop
method, switch it to false
while removing the dependencies.
export class ReactiveEffect<T = any> {
active = true // Added
//.
//.
//.
stop() {
if (this.active) {
this.active = false
}
}
}
With this basic implementation, all we need to do is remove all the dependencies when the stop
method is executed.
Additionally, let's add an implementation of hooks that allows us to register the processing we want to perform during cleanup, and handling when activeEffect
is itself.
export class ReactiveEffect<T = any> {
private deferStop?: boolean // Added
onStop?: () => void // Added
parent: ReactiveEffect | undefined = undefined // Added (to be referenced in finally)
run() {
if (!this.active) {
return this.fn() // If active is false, simply execute the function
}
try {
this.parent = activeEffect
activeEffect = this
const res = this.fn()
return res
} finally {
activeEffect = this.parent
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
if (activeEffect === this) {
// If activeEffect is itself, set a flag to stop after run is finished
this.deferStop = true
} else if (this.active) {
// ...
if (this.onStop) {
this.onStop() // Execute registered hooks
}
// ...
}
}
}
Now that we have added cleanup processing to ReactiveEffect, let's also implement the cleanup function for watch.
If the following code works, it's OK.
import { createApp, h, reactive, watch } from 'chibivue'
const app = createApp({
setup() {
const state = reactive({ count: 0 })
const increment = () => {
state.count++
}
const unwatch = watch(
() => state.count,
(newValue, oldValue, cleanup) => {
alert(`New value: ${newValue}, old value: ${oldValue}`)
cleanup(() => alert('Clean Up!'))
},
)
return () =>
h('div', {}, [
h('p', {}, [`count: ${state.count}`]),
h('button', { onClick: increment }, [`increment`]),
h('button', { onClick: unwatch }, [`unwatch`]),
])
},
})
app.mount('#app')
Source code so far:
chibivue (GitHub)
What is Effect Scope
Now that we can clean up effects, we want to clean up unnecessary effects when a component is unmounted. However, it is a bit cumbersome to collect a large number of effects, whether it's watch or computed. If we try to implement it straightforwardly, it will look like this:
let disposables = []
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
disposables.push(() => stop(doubled.effect))
const stopWatch = watchEffect(() => console.log(`counter: ${counter.value}`))
disposables.push(stopWatch)
// cleanup effects
disposables.forEach(f => f())
disposables = []
This kind of management is cumbersome and prone to mistakes.
Therefore, Vue has a mechanism called EffectScope.
https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md
The idea is to have one EffectScope per instance, and specifically, it has the following interface:
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// to dispose all effects in the scope
scope.stop()
Quoted from: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md#basic-example
And this EffectScope is also exposed as a user-facing API.
https://v3.vuejs.org/api/reactivity-advanced.html#effectscope
Implementation of EffectScope
As mentioned earlier, we will have one EffectScope per instance.
export interface ComponentInternalInstance {
scope: EffectScope
}
And when the component is unmounted, we stop the collected effects.
const unmountComponent = (...) => {
// .
// .
const { scope } = instance;
scope.stop();
// .
// .
}
The structure of EffectScope is as follows: it has a variable called activeEffectScope
that points to the currently active EffectScope, and it manages its state with the on/off/run/stop
methods implemented in EffectScope.
The on/off
methods lift themselves as activeEffectScope
or restore the lifted state (return to the original EffectScope).
And when a ReactiveEffect is created, it is registered in activeEffectScope
.
Since it may be a little difficult to understand, if we write the image in source code,
instance.scope.on()
/** Some ReactiveEffect such as computed or watch is created */
setup()
instance.scope.off()
With this, we can collect the generated effects in the EffectScope of the instance.
Then, when the stop
method of this effect is triggered, we can clean up all the effects.
You should have understood the basic principles, so let's try implementing it while reading the source code!
Source code so far:
chibivue (GitHub)