@fane_the_divine/react-signal 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # react-signal
2
+
3
+ 在react里使用signal, API参考solid,使用方法见[demo](src/index.tsx).
4
+
5
+ ## API介绍
6
+
7
+ ### defineComponent
8
+
9
+ ```tsx
10
+ type CompProps = {
11
+ num: number
12
+ }
13
+ const Comp = defineComponent<CompProps>((props) => {
14
+ return () => <span>{props().num}</span>
15
+ })
16
+ ```
17
+
18
+ defineComponent接受一个setup函数 这个函数在组件整个生命周期内只执行一次(不包括热重载)
19
+ setup应当返回一个render函数 这个函数会执行多次
20
+
21
+ ### createSignal
22
+
23
+ 创建signal
24
+
25
+ ```ts
26
+ const [count, setCount] = createSignal(0)
27
+
28
+ // 使用signal
29
+ count()
30
+
31
+ // 不具有响应性地获取signal里的值
32
+ count.value
33
+ ```
34
+
35
+ 可以在任何位置使用createSignal
36
+
37
+ ### createMemo
38
+
39
+ 创建一个派生signal
40
+
41
+ ```ts
42
+ // 参数为上一次的memo值
43
+ const double = createMemo((prev) => {
44
+ return count() * 2
45
+ // 第二个参数是自定义的比较眼熟
46
+ }, Object.is)
47
+
48
+ // 访问其值
49
+ double()
50
+
51
+ // 不具有响应性地获取其值
52
+ double.value
53
+ ```
54
+
55
+ 可以在任何位置使用createMemo
56
+ 派生signal会记录当前值是否过期 直到值被访问的时候才实际进行计算
57
+
58
+ ### createEffect
59
+
60
+ 创建基于signal的副作用,它们在react的useEffect阶段执行
61
+
62
+ ```ts
63
+ defineComponent(() => {
64
+ createEffect(
65
+ // 允许effect函数返回一个值 并会输入到下一次effect函数
66
+ // 可以从参数判断是否第一次调用
67
+ (prev, isFirst) => {
68
+ // 以非.value方式使用的signal会被收集 在其变化后 触发副作用
69
+ signal1()
70
+ return (prev ?? 0) + 1
71
+ },
72
+ // 允许提供一个signal数组作为额外的依赖
73
+ [signal2],
74
+ {
75
+ // 在useLayoutEffect阶段执行副作用
76
+ layoutEffect: true,
77
+ },
78
+ )
79
+ })
80
+ ```
81
+
82
+ createEffect应当在defineComponent内部调用
83
+ 关于它的清理函数 参考下一节
84
+
85
+ ### onCleanup
86
+
87
+ 提供清理函数
88
+
89
+ ```ts
90
+ defineComponent(() => {
91
+ createMemo(() => {
92
+ onCleanup(() => console.log('memo cleanup'))
93
+ })
94
+ createEffect(() => {
95
+ onCleanup(() => console.log('effect cleanup'))
96
+ })
97
+ onCleanup(() => console.log('component cleanup'))
98
+ })
99
+ ```
100
+
101
+ onCleanup可以在defineComponent createMemo createEffect内部调用 作用不同
102
+
103
+ - 在defineComponent内调用时 cleanup函数在组件销毁后调用
104
+ - createEffect/createMemo内调用时 cleanup会在effect/memo函数调用前调用
105
+
106
+ ### createRef
107
+
108
+ 创建一个react ref
109
+
110
+ ```tsx
111
+ defineComponent(() => {
112
+ const spanRef = createRef<HTMLSpanElement>()
113
+ return () => <span ref={spanRef} />
114
+ })
115
+ ```
116
+
117
+ 不提供参数时 类型是`RefObject<T|null>` 可以直接用到html上
118
+
119
+ ## 实现原理简述
120
+
121
+ 在[reactivity.ts](src/react-signal/reactivity.ts)定义了`响应式作用域`和`组件作用域`相关内容.
122
+
123
+ 通过全局变量的方式,可以注册一个`响应式作用域`。在它内部使用的signal变化后,`响应式作用域`可以感知这一变化并做出反应。例如组件会更新,派生signal将自身标记为已过期等。响应式作用域还可以感知内部的`onCleanup`调用,收集清理函数,并在自身销毁时调用清理函数。
124
+
125
+ `组件作用域`同样通过全局变量实现,它可以让函数在本组件的下一次useEffect阶段调用被调用.
126
+
127
+ defineComponent会创建响应式作用域和组件作用域。它会收集setup函数中的`onCleanup`调用,以及render函数中对signal的使用。
128
+
129
+ createMemo会创建响应式作用域。它不会立刻调用memo函数,而是在`signal()`或`signal.value`时才执行。在memo函数执行后,它会收集其使用到的signal,每次memo函数执行前都会清空上次收集的signal。在其依赖的signal变化后,它将自身标记为已过期,直到下一次自身的值被访问时才计算新值,然后向所有使用自身的`响应式作用域`发消息。值得注意的是,如果在render函数中访问了值过期的派生signal,它向组件的响应式作用域发消息,可以用变量判断当前是否处于rendering来进行区分,避免额外的更新。
130
+
131
+ createEffect在依赖收集方面与createMemo类似。在副作用实现上,createEffect确保函数`callEffectFn`在每个useEffect都执行。这个函数会访问`shouldCallEffect`变量,它为true时再执行effect函数,然后将此变量置为false。每当createEffect包裹的signal变化时,`shouldCallEffect`被置为true。
132
+
133
+ onCleanup获取当前的响应式作用域并将增加其清理函数。
134
+
135
+ createRef返回了一个普通对象。
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const d=require("react");let h;const y=()=>h;function E(o,t){const u=h;h=t;const s=o();return h=u,s}let p;const m=()=>p;function S(o,t){const u=p;p=t;const s=o();return p=u,s}function C(o){let t=o;const u=new Set;function s(){const n=y();return n&&!u.has(n)&&(u.add(n),n.listenRangeDestory(()=>u.delete(n))),t}Object.defineProperty(s,"value",{get(){return t}});function e(n){t!==n&&(t=n,u.forEach(l=>l.onSignalChange()))}return[s,e]}function v(o){const t=u=>{const[,s]=d.useReducer(()=>({}),null),e=d.useRef(null);if(e.current)e.current.propsSignal[1](u);else{const r={onSignalChange:()=>{var i;(i=e.current)!=null&&i.isRending||s()},listenRangeDestory:i=>{var c;(c=e.current)==null||c.cleanups.push(i)}},f={addEffectFn:i=>{var c;return(c=e.current)==null?void 0:c.effects.push(i)},addLayoutEffectFn:i=>{var c;return(c=e.current)==null?void 0:c.layoutEffects.push(i)}},g=C(u);e.current={reactivityRange:r,componentRange:f,cleanups:[],effects:[],layoutEffects:[],isRending:!1,propsSignal:g};const a=E(()=>S(()=>o(g[0]),f),r);e.current.render=a}d.useEffect(()=>()=>{if(!e.current)return;const r=e.current.cleanups;e.current=null,r.forEach(f=>f())},[]),d.useLayoutEffect(()=>{if(!e.current)return;const r=e.current.effects;e.current.effects=[],r.forEach(f=>f())}),d.useEffect(()=>{if(!e.current)return;const r=e.current.layoutEffects;e.current.layoutEffects=[],r.forEach(f=>f())});const{render:n,reactivityRange:l}=e.current;e.current.isRending=!0;const R=E(n,l);return e.current.isRending=!1,R};return d.memo(t)}function D(o,t,u){let s=[];function e(){s.forEach(c=>c()),s=[]}const n=y();n==null||n.listenRangeDestory(e);let l;const R={onSignalChange:()=>{f=!0},listenRangeDestory:c=>{s.push(c)}},r=m();let f=!0,g=!0;function a(){const{layoutEffect:c}=u??{};c?r==null||r.addLayoutEffectFn(i):r==null||r.addEffectFn(i)}function i(){a(),f&&(f=!1,e(),E(()=>{t==null||t.forEach(c=>c()),l=o(l,g),g=!1},R))}a()}function F(o,t=Object.is){let u=[];function s(){u.forEach(a=>a()),u=[]}const e=y();e==null||e.listenRangeDestory(s);let n,l=!0;const R={onSignalChange:()=>{l=!0},listenRangeDestory:a=>{u.push(a)}},r=new Set;function f(a=!0){if(!l)return;s();const i=n;E(()=>{n=o(i)},R),l=!1,!(!a||t(i,n))&&r.forEach(c=>c.onSignalChange())}function g(){f();const a=y();return a&&!r.has(a)&&(r.add(a),a.listenRangeDestory(()=>r.delete(a))),n}return Object.defineProperty(g,"value",{get(){return f(!1),n}}),g}function b(o){const t=y();t==null||t.listenRangeDestory(()=>E(o,void 0))}function M(o){return{current:o??null}}exports.createEffect=D;exports.createMemo=F;exports.createRef=M;exports.createSignal=C;exports.defineComponent=v;exports.onCleanup=b;
@@ -0,0 +1,159 @@
1
+ import { FC } from 'react';
2
+ import { MemoExoticComponent } from 'react';
3
+ import { ReactNode } from 'react';
4
+ import { RefObject } from 'react';
5
+
6
+ /**
7
+ * 创建基于 signal 的副作用;默认在 React `useEffect` 阶段执行。
8
+ *
9
+ * 应在 {@link defineComponent} 的 setup 内调用。清理逻辑使用 {@link onCleanup}。
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * defineComponent(() => {
14
+ * createEffect(
15
+ * // 允许 effect 函数返回一个值,并会输入到下一次 effect 函数
16
+ * // 可以从参数判断是否第一次调用
17
+ * (prev, isFirst) => {
18
+ * // 使用的 signal 会被收集,在其变化后触发副作用
19
+ * signal1()
20
+ * // .value访问值的不会被收集
21
+ * signal2.value
22
+ * // 下一次effect函数调用前或组件销毁时调用
23
+ * onCleanup(()=> console.log('effect'))
24
+ * return (prev ?? 0) + 1
25
+ * },
26
+ * // 允许提供一个 signal getter 数组作为额外的依赖
27
+ * [signal3],
28
+ * {
29
+ * // 在 useLayoutEffect 阶段执行副作用
30
+ * layoutEffect: true,
31
+ * },
32
+ * )
33
+ *
34
+ * return () => null
35
+ * })
36
+ * ```
37
+ */
38
+ export declare function createEffect<T>(effect: Effect<T>, deps?: Deps | undefined | null, option?: EffectOption): void;
39
+
40
+ /**
41
+ * 创建派生 signal;可在任意位置调用。
42
+ *
43
+ * 内部会记录值是否过期,直到 `getter()` 或 `.value` 访问时才实际重算。
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * // 参数为上一次的 memo 值
48
+ * const double = createMemo((prev) =>{
49
+ * // .value访问值的不会被收集
50
+ * signal2.value
51
+ * // count会被收集
52
+ * return count() * 2
53
+ * // 第二个参数是自定义的比较函数
54
+ * }, Object.is)
55
+ *
56
+ * // 访问其值
57
+ * double()
58
+ *
59
+ * // 不具有响应性地获取其值
60
+ * double.value
61
+ * ```
62
+ */
63
+ export declare function createMemo<T>(memo: Memo<T>, isEqual?: EqualFn<T>): SignalGetter<T>;
64
+
65
+ export declare function createRef<T>(defaultValue?: null | undefined): RefObject<T | null>;
66
+
67
+ export declare function createRef<T>(defaultValue: T): RefObject<T>;
68
+
69
+ export declare function createSignal<T>(): Signal<T | undefined>;
70
+
71
+ export declare function createSignal<T>(defaultValue: T): Signal<T>;
72
+
73
+ /**
74
+ * 使用 setup 创建支持响应式 signal 的组件。
75
+ *
76
+ * setup 在组件整个生命周期内只执行一次(不包括热重载),应返回一个 render 函数;render 会执行多次。
77
+ * 入参 `props` 为 {@link SignalGetter},在 setup 或 render 中用 `props()` 读取并追踪父组件传入的 props。
78
+ *
79
+ * @typeParam T - 组件 props 类型。
80
+ *
81
+ *
82
+ * @example
83
+ * ```tsx
84
+ * type CompProps = {
85
+ * num: number
86
+ * }
87
+ * const Comp = defineComponent<CompProps>((props) => {
88
+ * // props也是signal
89
+ * // 组件销毁后执行
90
+ * onCleanup(()=> console.log('comp unmounted'))
91
+ * // render中用到的signal被自动收集 变化后更新组件
92
+ * // 如果通过.value访问值 则不会被收集
93
+ * return () => <span>{props().num}</span>
94
+ * })
95
+ * ```
96
+ */
97
+ export declare function defineComponent<T = object>(sc: SignalComponent<T>): MemoExoticComponent<FC<T>>;
98
+
99
+ /** 额外依赖:一组 getter,在运行 effect 前会先调用以收集订阅。 */
100
+ export declare type Deps = (() => any)[];
101
+
102
+ /**
103
+ * effect 回调:返回值会作为下一次调用的 `prev`;`isFirst` 表示是否首次执行。
104
+ */
105
+ export declare type Effect<T> = (prev: T | undefined, isFirst: boolean) => T;
106
+
107
+ export declare type EffectOption = {
108
+ /** 为 true 时在 `useLayoutEffect` 阶段执行,否则在 `useEffect`。 */
109
+ layoutEffect?: boolean;
110
+ };
111
+
112
+ export declare type EqualFn<T> = (prev: T | undefined, current: T) => boolean;
113
+
114
+ /**
115
+ * `createMemo` 的计算函数;参数为上一轮的缓存值。
116
+ */
117
+ export declare type Memo<T> = (prev: T | undefined) => T;
118
+
119
+ /**
120
+ * 注册清理函数。
121
+ *
122
+ * 可在 {@link defineComponent}、{@link createMemo}、{@link createEffect} 内调用,语义不同:
123
+ * - 在 **defineComponent** 内:`cleanup` 在组件销毁后调用。
124
+ * - 在 **createEffect** / **createMemo** 内:`cleanup` 在 effect / memo **下一次执行前**调用。
125
+ *
126
+ * 执行清理时不在响应式上下文中,其中读取 signal 不会建立订阅。
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * defineComponent(() => {
131
+ * createMemo(() => {
132
+ * onCleanup(() => console.log('memo cleanup'))
133
+ * return 0
134
+ * })
135
+ * createEffect(() => {
136
+ * onCleanup(() => console.log('effect cleanup'))
137
+ * })
138
+ * onCleanup(() => console.log('component cleanup'))
139
+ *
140
+ * return () => null
141
+ * })
142
+ * ```
143
+ */
144
+ export declare function onCleanup(cleanup: () => void): void;
145
+
146
+ export declare type Render = () => ReactNode;
147
+
148
+ export declare type Signal<T> = [SignalGetter<T>, (val: T) => void];
149
+
150
+ declare type SignalComponent<T = object> = (props: SignalGetter<T>) => Render;
151
+ export { SignalComponent as SC }
152
+ export { SignalComponent }
153
+
154
+ export declare type SignalGetter<T> = {
155
+ (): T;
156
+ value: T;
157
+ };
158
+
159
+ export { }
@@ -0,0 +1,173 @@
1
+ import { memo as C, useReducer as m, useRef as v, useEffect as p, useLayoutEffect as S } from "react";
2
+ let h;
3
+ const d = () => h;
4
+ function y(a, r) {
5
+ const u = h;
6
+ h = r;
7
+ const s = a();
8
+ return h = u, s;
9
+ }
10
+ let E;
11
+ const D = () => E;
12
+ function F(a, r) {
13
+ const u = E;
14
+ E = r;
15
+ const s = a();
16
+ return E = u, s;
17
+ }
18
+ function V(a) {
19
+ let r = a;
20
+ const u = /* @__PURE__ */ new Set();
21
+ function s() {
22
+ const t = d();
23
+ return t && !u.has(t) && (u.add(t), t.listenRangeDestory(() => u.delete(t))), r;
24
+ }
25
+ Object.defineProperty(s, "value", {
26
+ get() {
27
+ return r;
28
+ }
29
+ });
30
+ function e(t) {
31
+ r !== t && (r = t, u.forEach((l) => l.onSignalChange()));
32
+ }
33
+ return [s, e];
34
+ }
35
+ function L(a) {
36
+ return C((u) => {
37
+ const [, s] = m(() => ({}), null), e = v(null);
38
+ if (e.current)
39
+ e.current.propsSignal[1](u);
40
+ else {
41
+ const n = {
42
+ onSignalChange: () => {
43
+ var i;
44
+ (i = e.current) != null && i.isRending || s();
45
+ },
46
+ listenRangeDestory: (i) => {
47
+ var c;
48
+ (c = e.current) == null || c.cleanups.push(i);
49
+ }
50
+ }, f = {
51
+ addEffectFn: (i) => {
52
+ var c;
53
+ return (c = e.current) == null ? void 0 : c.effects.push(i);
54
+ },
55
+ addLayoutEffectFn: (i) => {
56
+ var c;
57
+ return (c = e.current) == null ? void 0 : c.layoutEffects.push(i);
58
+ }
59
+ }, g = V(u);
60
+ e.current = {
61
+ reactivityRange: n,
62
+ componentRange: f,
63
+ cleanups: [],
64
+ effects: [],
65
+ layoutEffects: [],
66
+ isRending: !1,
67
+ propsSignal: g
68
+ };
69
+ const o = y(
70
+ () => F(() => a(g[0]), f),
71
+ n
72
+ );
73
+ e.current.render = o;
74
+ }
75
+ p(() => () => {
76
+ if (!e.current) return;
77
+ const n = e.current.cleanups;
78
+ e.current = null, n.forEach((f) => f());
79
+ }, []), S(() => {
80
+ if (!e.current) return;
81
+ const n = e.current.effects;
82
+ e.current.effects = [], n.forEach((f) => f());
83
+ }), p(() => {
84
+ if (!e.current) return;
85
+ const n = e.current.layoutEffects;
86
+ e.current.layoutEffects = [], n.forEach((f) => f());
87
+ });
88
+ const { render: t, reactivityRange: l } = e.current;
89
+ e.current.isRending = !0;
90
+ const R = y(t, l);
91
+ return e.current.isRending = !1, R;
92
+ });
93
+ }
94
+ function b(a, r, u) {
95
+ let s = [];
96
+ function e() {
97
+ s.forEach((c) => c()), s = [];
98
+ }
99
+ const t = d();
100
+ t == null || t.listenRangeDestory(e);
101
+ let l;
102
+ const R = {
103
+ onSignalChange: () => {
104
+ f = !0;
105
+ },
106
+ listenRangeDestory: (c) => {
107
+ s.push(c);
108
+ }
109
+ }, n = D();
110
+ let f = !0, g = !0;
111
+ function o() {
112
+ const { layoutEffect: c } = u ?? {};
113
+ c ? n == null || n.addLayoutEffectFn(i) : n == null || n.addEffectFn(i);
114
+ }
115
+ function i() {
116
+ o(), f && (f = !1, e(), y(() => {
117
+ r == null || r.forEach((c) => c()), l = a(l, g), g = !1;
118
+ }, R));
119
+ }
120
+ o();
121
+ }
122
+ function j(a, r = Object.is) {
123
+ let u = [];
124
+ function s() {
125
+ u.forEach((o) => o()), u = [];
126
+ }
127
+ const e = d();
128
+ e == null || e.listenRangeDestory(s);
129
+ let t, l = !0;
130
+ const R = {
131
+ onSignalChange: () => {
132
+ l = !0;
133
+ },
134
+ listenRangeDestory: (o) => {
135
+ u.push(o);
136
+ }
137
+ }, n = /* @__PURE__ */ new Set();
138
+ function f(o = !0) {
139
+ if (!l) return;
140
+ s();
141
+ const i = t;
142
+ y(() => {
143
+ t = a(i);
144
+ }, R), l = !1, !(!o || r(i, t)) && n.forEach((c) => c.onSignalChange());
145
+ }
146
+ function g() {
147
+ f();
148
+ const o = d();
149
+ return o && !n.has(o) && (n.add(o), o.listenRangeDestory(() => n.delete(o))), t;
150
+ }
151
+ return Object.defineProperty(g, "value", {
152
+ get() {
153
+ return f(!1), t;
154
+ }
155
+ }), g;
156
+ }
157
+ function M(a) {
158
+ const r = d();
159
+ r == null || r.listenRangeDestory(() => y(a, void 0));
160
+ }
161
+ function O(a) {
162
+ return {
163
+ current: a ?? null
164
+ };
165
+ }
166
+ export {
167
+ b as createEffect,
168
+ j as createMemo,
169
+ O as createRef,
170
+ V as createSignal,
171
+ L as defineComponent,
172
+ M as onCleanup
173
+ };
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@fane_the_divine/react-signal",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "在react中使用signal",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/FanetheDivine/react-signal.git"
10
+ },
11
+ "keywords": [
12
+ "react",
13
+ "signals",
14
+ "reactivity"
15
+ ],
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "main": "./dist/react-signal.cjs",
21
+ "module": "./dist/react-signal.js",
22
+ "types": "./dist/react-signal.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/react-signal.d.ts",
26
+ "import": "./dist/react-signal.js",
27
+ "require": "./dist/react-signal.cjs"
28
+ }
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0",
32
+ "react-dom": ">=18.0.0"
33
+ },
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build-publish": "vite build --config vite.lib.config.ts && npm publish --access public",
37
+ "lint": "eslint src/ ",
38
+ "prettier": "prettier --write src/"
39
+ },
40
+ "devDependencies": {
41
+ "react": "^19",
42
+ "react-dom": "^19",
43
+ "@eslint/js": "^9.22.0",
44
+ "@tailwindcss/vite": "^4.1.11",
45
+ "@trivago/prettier-plugin-sort-imports": "^5.2.2",
46
+ "@types/node": "^20",
47
+ "@types/react": "^19",
48
+ "@types/react-dom": "^19",
49
+ "@vitejs/plugin-react-swc": "^3.11.0",
50
+ "eslint": "9",
51
+ "eslint-plugin-react-hooks": "^5.2.0",
52
+ "eslint-plugin-react-refresh": "^0.4.19",
53
+ "globals": "^16.3.0",
54
+ "prettier": "3.6.2",
55
+ "prettier-plugin-tailwindcss": "0.6.14",
56
+ "tailwindcss": "^4",
57
+ "typescript": "^5",
58
+ "typescript-eslint": "^8.26.1",
59
+ "vite": "^6",
60
+ "vite-plugin-dts": "^4.5.4"
61
+ },
62
+ "pnpm": {
63
+ "onlyBuiltDependencies": [
64
+ "@swc/core"
65
+ ]
66
+ }
67
+ }