$watch & $stopEffects
Sensor manages its own watcher: start() registers a $watch in the instance's lazily created effect scope, stop() disposes just that watcher, and dispose() calls $stopEffects() — the scope stops and every cached cell is dropped, so state re-materializes fresh on the next access.
What to notice
- The scope is lazy. An instance that never calls
$watchallocates no effect scope at all. - Dispose is total. After
$stopEffects(), the old cells are gone; touching any ref-getter materializes a fresh cell. - The watch callback delegates to a method (
onTempChanged) — the thin-closure rule keeps logic named on the prototype and directly testable.
The source
ts
// Sensor.ts — $watch and $stopEffects on an instance you control by hand.
import { ref } from 'vue';
import { Reactive } from '../../ivue';
class $Sensor {
get temp() {
return ref(20);
}
get watching() {
return ref(false);
}
get fired() {
return ref(0);
}
get lastChange() {
return ref('');
}
#stopWatch?: () => void;
start() {
if (this.watching.value) return;
this.watching.value = true;
// $watch registers in the instance's lazy effect scope
this.#stopWatch = (this as any).$watch(
() => this.temp.value,
(newTemp: number, oldTemp: number) => this.onTempChanged(newTemp, oldTemp),
);
}
stop() {
this.#stopWatch?.();
this.#stopWatch = undefined;
this.watching.value = false;
}
dispose() {
(this as any).$stopEffects(); // stops the scope, clears every cached cell
this.watching.value = false; // fresh cells materialize on next access
}
onTempChanged(newTemp: number, oldTemp: number) {
this.fired.value++;
this.lastChange.value = `${oldTemp} → ${newTemp}`;
}
}
export namespace Sensor {
export const $Class = $Sensor; // raw — children `extends` this
export let Class = Reactive($Class); // reactive — you `new` this
export type Instance = typeof Class.Instance; // defineExpose type & reactive() interop
}vue
<script setup lang="ts">
import { onUnmounted } from 'vue';
import { Sensor } from './Sensor';
const sensor = new Sensor.Class();
// the state destructure
const {
// state refs
temp,
watching,
fired,
lastChange,
} = sensor;
onUnmounted(() => sensor.$stopEffects());
</script>
<template>
<div class="pane">
<p class="note">
Start registers a watcher in the instance's lazily created effect scope.
Dispose calls $stopEffects: the scope stops and every cached cell is
dropped, so state re-materializes fresh.
</p>
<div class="vals">
<div>
<div class="k">temp</div>
<div class="n">{{ temp }}°</div>
</div>
<div>
<div class="k">watcher</div>
<div class="n" :class="watching ? 'grad' : ''">
{{ watching ? 'ON' : 'off' }}
</div>
</div>
<div>
<div class="k">fired</div>
<div class="n">{{ fired }}×</div>
</div>
<div>
<div class="k">last change</div>
<div class="n mono">{{ lastChange || '—' }}</div>
</div>
</div>
<div class="row">
<input
class="slider"
type="range"
min="0"
max="40"
v-model.number="temp"
aria-label="temperature"
/>
<button class="btn primary" type="button" @click="sensor.start()">
start
</button>
<button class="btn" type="button" @click="sensor.stop()">stop</button>
<button class="btn" type="button" @click="sensor.dispose()">
dispose
</button>
</div>
</div>
</template>
<style scoped src="../example-pane.css"></style>Open in StackBlitz ⚡ — the playground boots with this example's route and file active.