iter(g217): 补 frame 内核冒烟并修复 effect 注销

This commit is contained in:
HIsheR
2026-10-26 19:58:40 +08:00
parent d7ad79963b
commit 34985fff43
4 changed files with 119 additions and 5 deletions
+99
View File
@@ -0,0 +1,99 @@
/*** 作用:@ui-arch/frame 内核冒烟——信号 / 副作用 / 计算值的语义断言(纯 JS,无需 DOM;跑 npm run smoke 执行) ***/
import { computed, effect, signal } from '../dist/signal.js';
let pass = 0;
let fail = 0;
/*** 断言:失败也继续跑完,最后统一给退出码(否则第一个红就看不到后面) ***/
const ok = (label, cond) => {
if (cond) {
pass += 1;
console.log(`ok - ${label}`);
return;
}
fail += 1;
console.error(`NOT OK - ${label}`);
};
// —— 1. 信号的读写与同值短路 ——
const count = signal(0);
let hits = 0;
effect(() => {
count();
hits += 1;
});
ok('effect 创建时立即执行一次', hits === 1);
count(1);
ok('写值后重跑', hits === 2);
count(1);
ok('写入相同值不触发(Object.is 短路)', hits === 2);
// —— 2. 注销后不再重跑 ——
const live = signal('a');
let liveHits = 0;
const dispose = effect(() => {
live();
liveHits += 1;
});
live('b');
ok('注销前正常重跑', liveHits === 2);
dispose();
live('c');
ok('注销后写值不再重跑(dispose 必须真的摘订阅)', liveHits === 2);
// —— 3. 条件分支重追踪:上一轮读过、这一轮不读的信号不该再唤它 ——
const flag = signal(true);
const left = signal(0);
const right = signal(0);
let branchHits = 0;
effect(() => {
branchHits += 1;
if (flag()) left();
else right();
});
const afterInit = branchHits;
flag(false);
ok('切分支本身触发一次重跑', branchHits === afterInit + 1);
const afterSwitch = branchHits;
left(9);
ok('切走后旧的依赖不再唤它(重追踪要清旧订阅)', branchHits === afterSwitch);
right(9);
ok('切走后新依赖仍唤它', branchHits === afterSwitch + 1);
// —— 4. computed:派生、链式、同值短路 ——
const price = signal(10);
const qty = signal(2);
const total = computed(() => price() * qty());
ok('computed 首次读即得派生值', total() === 20);
price(3);
ok('源变化后 computed 立即跟着变', total() === 6);
let derivedHits = 0;
effect(() => {
total();
derivedHits += 1;
});
const derivedBase = derivedHits;
qty(3);
ok('computed 变更会唤上层 effect(链式依赖成立)', derivedHits === derivedBase + 1);
ok('链式计算值正确', total() === 9);
// —— 5. 多订阅者:扇出与互不影响 ——
const fan = signal(0);
let a = 0;
let b = 0;
const disposeA = effect(() => {
fan();
a += 1;
});
effect(() => {
fan();
b += 1;
});
fan(1);
ok('一次写值唤全部订阅者', a === 2 && b === 2);
disposeA();
fan(2);
ok('注销一个订阅者不影响另一个', a === 2 && b === 3);
console.log(`\n${fail === 0 ? 'SMOKE OK' : 'SMOKE FAILED'}(${pass} 通过 / ${fail} 失败)`);
process.exit(fail === 0 ? 0 : 1);
+18 -3
View File
@@ -1,5 +1,7 @@
// 当前正在收集依赖的 effect;null 表示非追踪态
let active: (() => void) | null = null;
// 当前 effect 已订阅到的「信号订阅集」集合:重跑前据此清旧订阅,注销时据此摘干净
let activeDeps: Set<Set<() => void>> | null = null;
/*** 响应式信号:无参调用读值并收集依赖,传参写值并触发订阅 ***/
export interface Signal<T> {
@@ -13,7 +15,10 @@ export function signal<T>(initial: T): Signal<T> {
const subs = new Set<() => void>();
const read = (...args: unknown[]) => {
if (args.length === 0) {
if (active) subs.add(active); // 依赖收集
if (active) {
subs.add(active); // 依赖收集
activeDeps?.add(subs); // 同时记下「我订阅了哪个集」,注销时才能摘干净
}
return value;
}
const v = args[0] as T;
@@ -24,20 +29,30 @@ export function signal<T>(initial: T): Signal<T> {
return read as Signal<T>;
}
/*** 副作用:立即执行一次并追踪内部读取的信号,依赖变化自动重跑 ***/
/*** 副作用:立即执行一次并追踪内部读取的信号,依赖变化自动重跑;返回注销函数 ***/
export function effect(fn: () => void): () => void {
const deps = new Set<Set<() => void>>(); // 本 effect 订阅到的所有信号订阅集
let stopped = false; // 注销标志:注销后即使还在某个订阅集里也不再执行
const run = () => {
if (stopped) return; // 卫语句:注销后不再跑
for (const subs of deps) subs.delete(run); // 清上一轮订阅:条件分支换掉后的旧依赖不该再唤它
deps.clear();
const prev = active;
const prevDeps = activeDeps;
active = run;
activeDeps = deps;
try {
fn();
} finally {
active = prev;
activeDeps = prevDeps;
}
};
run();
return () => {
/* 演示级实现:不做依赖清理 */
stopped = true;
for (const subs of deps) subs.delete(run); // 真摘订阅:否则卸载后的组件仍会被信号唤醒
deps.clear();
};
}