Releases: stackblitz/alien-signals
v2.0.0
Version 2.0 rethinks and optimizes the reactive system model, with minimal impact on users who only use the surface APIs. If you extend or deeply utilize alien-signals
, please pay attention to the "Reactive Model Refactor" section.
Changes to Surface APIs
- Added four new APIs:
getCurrentSub
,setCurrentSub
,getCurrentScope
,setCurrentScope
Deferred Signal Value Evaluation
Differences from v1
In v1, assigning a value to a signal immediately propagated the
Dirty
flag, causing some computed values to be unnecessarily re-evaluated.
// v1
const src = signal(10);
const double = computed(() => src() * 2);
double(); // -> 20
src(999); // double.flags -> Dirty
src(10); // no effect
double(); // -> 20 (recomputed unnecessarily)
In v2, assigning a value to a signal only propagates the
Pending
flag, reducing unnecessary recomputation. Actual evaluation occurs during the next read.
// v2
const src = signal(10);
const double = computed(() => src() * 2);
double(); // -> 20
src(999); // src.flags -> Dirty, double.flags -> Pending
src(10); // no effect
double(); // Checks src state -> unchanged, no recomputation needed
Effect Scope Parent-Child Hierarchy
In v2, recursive cleanup is achieved through a parent-child structure:
const scope1 = effectScope(() => {
const scope2 = effectScope(() => {
effect(() => ...);
computed(() => ...);
});
});
scope1();
Calling scope1()
automatically cleans up its child scope scope2
.
To make scope2
independent of scope1
, temporarily set activeScope = undefined
manually:
const scope1 = effectScope(() => {
const prev = setCurrentScope(undefined);
const scope2 = effectScope(() => {
effect(() => ...);
computed(() => ...);
});
setCurrentScope(prev);
});
Reactive Model Refactor
- Merged
Subscriber
andDependency
intoReactiveNode
- Trigger
unwatched(dep)
when all subscribers are lost, without recursively clearing subsequent subscribers propagate
now only propagatesPending
; to immediately markDirty
, callpropagate
+shallowPropagate
after assignment- Adjusted naming for
EffectFlags
andReactiveFlags
; removed unused flags
Updated Options and APIs
notifyEffect
βnotify
updateComputed
βupdate
- Added the
unwatched
option for custom handling when all subscribers are lost - Removed
processEffectNotifications
,processComputedUpdate
,processPendingInnerEffects
,updateDirtyFlag
- Added
unlink
,checkDirty
For performance differences, please refer to js-reactivity-benchmark.
Contributors
- @johnsoncodehk
- @sxzz
- @medz
- @wangshunnn
- @jh-leong
- @akshar-dave
- @transitive-bullshit
- @PuruVJ
- @amb26
- @tomByrer
- @zhangenming
- @yamanoku
- @Nicell
- @AimWhy
- @Mox93
Thanks~
v1.0.0
New createReactiveSystem()
API
The core algorithm is now decoupled from the public API, and you can redesign your surface API using the alien-signals algorithm.
(The following example is based on proposal-signals/signal-polyfill#44)
const system = createReactiveSystem({
updateComputed(computed: Computed) {
return computed.update();
},
notifyEffect(watcher: subtle.Watcher) {
if (watcher.flags & alien.SubscriberFlags.Dirty) {
watcher.run();
return true;
}
return false;
},
});
signal, computed, effect, effectScope are now function-based instead of class-based
The class-based API design can minimize memory usage, but at the same time I think the get()
and set()
methods of class signal are also significantly worse in DX.
Since the core algorithm is now decoupled from the surface API, we decided to stop caring about scalability and memory footprint and switch to a function-based API to ensure proper DX by default.
For the latest API usage, please refer to the readme: https://github.com/stackblitz/alien-signals#usage
Performance improved by 12% compared to v0.6
With the 1.0 refactor changes, we have again seen performance improvements in our internal benchmark scripts in alien-signals.
Please note that this improvement is only theoretical since the --jitless
flag is disabled in the alien-signals bench script, but it is a good thing anyway. :)
