[Signal] 2- Cleanup subscriptions
We have a basic version of signal:
const stack = []
export function createSignal(value) {
const subscribers = new Set();
const read = () => {
// check is there any effect runnning?
// if yes, then add to subscribers
const observer = stack[stack.length - 1];
if (observer) {
subscribers.add(observer);
}
return value;
};
const write = (newValue) => {
value = newValue;
// notify all the subscribers there is changes
for (const observer of subscribers) {
// add effect to the stack again
observer.execute();
}
};
return [read, write];
}
export function createEffect(fn) {
const effect = {
execute() {
stack.push(effect);
fn();
stack.pop();
}
};
effect.execute();
}
One problem of current version is that, subscriptions are not cleanup. Therefore it might be triggered when not necessary.
import { createSignal, createEffect } from "./reactive";
const [count, setCount] = createSignal(0);
const [count2, setCount2] = createSignal(2);
const [show, setShow] = createSignal(true);
createEffect(() => {
if (show()) console.log(count());
else console.log(count2());
});
setShow(false);
setCount(10);
// 0
// 2
// 2
It logs 2
twice. Which is a signals cleanup is missing.
Version 3: with cleanup
In previous version, signals knows effect, but effect doesn't know signals.
We need to link bidirectional.
export function createEffect(fn) {
const effect = {
execute() {
context.push(effect);
fn();
context.pop();
},
dependencies: new Set(), // add dependencies to link to signals
};
effect.execute();
}
When read from signals, we need to not only link effect to signals, but also link signals to effect:
function subscribe(observer, subscriptions) {
subscriptions.add(observer)
observer.dependencies.add(subscriptions)
}
export function createSignal(value) {
const subscriptions = new Set();
const read = () => {
const observer = context[context.length - 1];
if (observer) subscribe(observer, subscriptions); // link bidirectional
return value;
};
const write = (newValue) => {
value = newValue;
for (const observer of [...subscriptions]) { // need to use [...subscriptions] to make a copy to avoid infinte loop
observer.execute();
}
};
return [read, write];
}
Write cleanup:
function cleanup(observer) {
for (const dep of observer.dependencies) {
dep.delete(observer);
}
}
export function createEffect(fn) {
const effect = {
execute() {
cleanup(effect); // call cleanup before next effect start
context.push(effect);
fn();
context.pop();
},
dependencies: new Set(),
};
effect.execute();
}
---FULL CODE---
const context = [];
function cleanup(observer) {
for (const dep of observer.dependencies) {
dep.delete(observer);
}
}
function subscribe(observer, subscriptions) {
subscriptions.add(observer);
observer.dependencies.add(subscriptions);
}
export function createSignal(value) {
const subscriptions = new Set();
const read = () => {
const observer = context[context.length - 1];
if (observer) subscribe(observer, subscriptions);
return value;
};
const write = (newValue) => {
value = newValue;
for (const observer of [...subscriptions]) {
observer.execute();
}
};
return [read, write];
}
export function createEffect(fn) {
const effect = {
execute() {
cleanup(effect);
context.push(effect);
fn();
context.pop();
},
dependencies: new Set(),
};
effect.execute();
}
Test code:
import { createSignal, createEffect } from "./reactive";
const [count, setCount] = createSignal(0);
const [count2, setCount2] = createSignal(2);
const [show, setShow] = createSignal(true);
createEffect(() => {
if (show()) console.log(count());
else console.log(count2());
});
setShow(false);
setCount(10);
// 0
// 2