From 854d72a420667382af2879903f20b120d0772da5 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 28 Jun 2026 21:32:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(framework):=20core=20=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BC=8F=20signal=20=E5=9F=BA=E5=85=83(=E8=87=AA=E7=A0=94?= =?UTF-8?q?=E8=96=84=E5=B0=81=E8=A3=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../assets/framework/core/reactive.ts | 31 +++++++++++++ .../framework-tests/platform/reactive.test.ts | 45 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts create mode 100644 cocoscreator_projects/framework-tests/platform/reactive.test.ts diff --git a/cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts b/cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts new file mode 100644 index 0000000..b4bfdc0 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/core/reactive.ts @@ -0,0 +1,31 @@ +export type Unsubscribe = () => void; + +export interface ReadonlyReactive { + readonly value: T; + subscribe(fn: (value: T, prev: T) => void): Unsubscribe; // 返回退订函数 +} + +export interface Reactive extends ReadonlyReactive { + value: T; // 可写 +} + +/** 极小响应式信号:value 读写 + 订阅。Object.is 判等(值未变不通知);订阅者抛错隔离。 */ +export function signal(initial: T): Reactive { + let current = initial; + const subs = new Set<(value: T, prev: T) => void>(); + return { + get value(): T { return current; }, + set value(v: T) { + if (Object.is(v, current)) return; + const prev = current; + current = v; + for (const fn of [...subs]) { + try { fn(current, prev); } catch (e) { console.error('[reactive] subscriber error:', e); } + } + }, + subscribe(fn: (value: T, prev: T) => void): Unsubscribe { + subs.add(fn); + return () => { subs.delete(fn); }; + }, + }; +} diff --git a/cocoscreator_projects/framework-tests/platform/reactive.test.ts b/cocoscreator_projects/framework-tests/platform/reactive.test.ts new file mode 100644 index 0000000..3567741 --- /dev/null +++ b/cocoscreator_projects/framework-tests/platform/reactive.test.ts @@ -0,0 +1,45 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { signal } from '../../YouleNexus/assets/framework/core/reactive.ts'; + +test('signal: value 读写', () => { + const s = signal(1); + assert.equal(s.value, 1); + s.value = 2; + assert.equal(s.value, 2); +}); + +test('signal: subscribe 收到 (value, prev)', () => { + const s = signal(1); + let got: [number, number] | null = null; + s.subscribe((v, p) => { got = [v, p]; }); + s.value = 5; + assert.deepEqual(got, [5, 1]); +}); + +test('signal: 退订后不再收到', () => { + const s = signal(0); + let count = 0; + const un = s.subscribe(() => { count++; }); + s.value = 1; + un(); + s.value = 2; + assert.equal(count, 1); +}); + +test('signal: Object.is 判等,值未变不通知', () => { + const s = signal(7); + let count = 0; + s.subscribe(() => { count++; }); + s.value = 7; + assert.equal(count, 0); +}); + +test('signal: 一个订阅者抛错不影响其它', () => { + const s = signal(0); + let reached = false; + s.subscribe(() => { throw new Error('boom'); }); + s.subscribe(() => { reached = true; }); + s.value = 1; + assert.equal(reached, true); +});