├── .vscode
└── settings.json
├── main.ts
├── jsx.ts
├── ssr.tsx
├── signal_test.ts
├── signal.mjs
└── README.md
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true
3 | }
4 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { html } from "https://deno.land/x/html@v1.2.0/mod.ts";
2 | import { serve } from "https://deno.land/std@0.181.0/http/server.ts";
3 |
4 | async function handler(req: Request) {
5 | if (req.url.endsWith(".mjs")) {
6 | return new Response(await Deno.readTextFile("./signal.mjs"), {
7 | headers: { "content-type": "text/javascript; charset=utf-8" },
8 | });
9 | }
10 |
11 | const body = html`
12 |
13 |
24 | `;
25 | return new Response(body, {
26 | headers: { "content-type": "text/html; charset=utf-8" },
27 | });
28 | }
29 |
30 | serve(handler);
31 |
--------------------------------------------------------------------------------
/jsx.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 | export interface VNode {
7 | tag: string;
8 | attrs: Record;
9 | children: VNode[];
10 | }
11 |
12 | export function h(
13 | tag: string,
14 | attrs: Record,
15 | ...children: VNode[]
16 | ): VNode {
17 | return { tag, attrs, children };
18 | }
19 |
20 | export function renderToString(vdom: VNode | string | number): string {
21 | if (typeof vdom === "string" || typeof vdom === "number") {
22 | return String(vdom);
23 | }
24 |
25 | const attrs = vdom.attrs
26 | ? Object.keys(vdom.attrs)
27 | .map((key) => ` ${key}="${vdom.attrs[key]}"`)
28 | .join("")
29 | : "";
30 | const children = vdom.children.map((child) => renderToString(child)).join("");
31 |
32 | return `<${vdom.tag}${attrs}>${children}${vdom.tag}>`;
33 | }
34 |
35 | declare global {
36 | namespace JSX {
37 | interface IntrinsicElements {
38 | [key: string]: unknown;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ssr.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 |
3 | import { serve } from "https://deno.land/std@0.140.0/http/server.ts";
4 | import { h, renderToString } from "./jsx.ts";
5 |
6 | function Counter() {
7 | return ;
8 | }
9 |
10 | async function handler(req: Request) {
11 | if (req.url.endsWith(".mjs")) {
12 | return new Response(await Deno.readTextFile("./signal.mjs"), {
13 | headers: { "content-type": "text/javascript; charset=utf-8" },
14 | });
15 | }
16 |
17 | const page = (
18 |
19 | Writing Your Own Reactive Signal Library
20 |
21 |
34 |
35 | );
36 | const html = renderToString(page);
37 | return new Response(html, {
38 | headers: { "content-type": "text/html; charset=utf-8" },
39 | });
40 | }
41 |
42 | serve(handler);
43 |
--------------------------------------------------------------------------------
/signal_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertSpyCall,
3 | assertSpyCalls,
4 | spy,
5 | } from "https://deno.land/std@0.181.0/testing/mock.ts";
6 | import { assertEquals } from "https://deno.land/std@0.181.0/testing/asserts.ts";
7 | import { createEffect, createSignal } from "./signal.mjs";
8 |
9 | Deno.test("createSignal should return a tuple with a read function", () => {
10 | const [count] = createSignal(0);
11 | assertEquals(count(), 0);
12 | });
13 |
14 | Deno.test("createSignal should return a tuple with a write function", () => {
15 | const [count, setCount] = createSignal(0);
16 | setCount(1);
17 | assertEquals(count(), 1);
18 | setCount(2);
19 | assertEquals(count(), 2);
20 | });
21 |
22 | Deno.test("createEffect should call the callback function", () => {
23 | const mockCallback = spy(() => {});
24 | createEffect(mockCallback);
25 |
26 | assertSpyCall(mockCallback, 0, {
27 | args: [],
28 | returned: undefined,
29 | });
30 | assertSpyCalls(mockCallback, 1);
31 | });
32 |
33 | Deno.test("createSignal should update value and notify subscribers", () => {
34 | const [count, setCount] = createSignal(0);
35 | const mockSubscriber = spy(() => {});
36 | createEffect(() => {
37 | mockSubscriber();
38 | count();
39 | });
40 | setCount(1);
41 | assertEquals(count(), 1);
42 | assertSpyCalls(mockSubscriber, 2);
43 | });
44 |
--------------------------------------------------------------------------------
/signal.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * 存储当前正在监听响应式值变化的函数
3 | * @type {(() => void) | undefined}
4 | */
5 | let currentListener = undefined;
6 |
7 | /**
8 | * @template T
9 | * @typedef {() => T} ReadSignal
10 | * @typedef {(newValue: T) => void} WriteSignal
11 | * @typedef {[ReadSignal, WriteSignal]} Signal
12 | */
13 |
14 | /**
15 | * `createSignal` 用于读取和设置响应式的值。
16 | *
17 | * 示例用法:
18 | *
19 | * ```ts
20 | * const [count, setCount] = createSignal(0);
21 | * ```
22 | *
23 | * @param {T} initialValue - 初始化的值
24 | * @returns {Signal} - 返回读写响应式值的函数组成的数组
25 | */
26 | export function createSignal(initialValue) {
27 | let value = initialValue;
28 |
29 | /**
30 | * 存储 createEffect 中传入的函数,这些函数会在值变化时被调用
31 | * @type {Set<() => void>}
32 | */
33 | const subscribers = new Set();
34 |
35 | // 定义读取函数,如果当前正在监听,则将监听器函数存入 subscribers 中
36 | const read = () => {
37 | if (currentListener !== undefined) {
38 | subscribers.add(currentListener);
39 | }
40 | return value;
41 | };
42 |
43 | /**
44 | * @param {T} newValue - 要设置的新值
45 | */
46 | const write = (newValue) => {
47 | value = newValue;
48 | // 值变化后,遍历 subscribers 调用每个监听器函数
49 | subscribers.forEach((fn) => fn());
50 | };
51 |
52 | return [read, write];
53 | }
54 |
55 | /**
56 | * `createEffect` 用于在响应式值发生变化时执行副作用。
57 | *
58 | * 每当响应式值变化时,它都会运行传入的回调函数。
59 | *
60 | * 示例用法:
61 | *
62 | * ```ts
63 | * createEffect(() => {
64 | * console.log(someSignal());
65 | * });
66 | * ```
67 | *
68 | * @param {() => void} callback - 要执行的回调函数
69 | */
70 | export function createEffect(callback) {
71 | currentListener = callback; // 记录当前正在运行的监听器函数
72 | callback(); // 在当前监听器函数下执行回调函数
73 | currentListener = undefined; // 在不需要监听器函数时避免将其添加到订阅者集合中
74 | }
75 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Writing Your Own Reactive Signal Library
2 |
3 | 文章
4 | [Writing Your Own Reactive Signal Library](https://www.lksh.dev/blog/writing-your-own-reactive-signal-library/)
5 |
6 | 本仓库是它的源码和 Demo。
7 |
8 | ## 概述
9 |
10 | 这篇文章讨论了最近前端界对于精细化反应性的新兴关注,精细化反应性是一种通过使用三个主要的原语(signal、effect
11 | 和 memo)来构建反应式用户界面的方式。近期,像 Angular、Preact 和 Qwik
12 | 等框架也加入了 signal 的支持,当然,SolidJS
13 | 作为作者的首选框架,也在前端框架中引领了 signal 的流行趋势。
14 |
15 | 接着文章介绍了 signal 是什么以及为什么需要创建自己的 signal。Signal
16 | 是事件发射器,它们包含一系列订阅。当信号值发生变化时,它们会通知它们的订阅者。为了更好地理解
17 | SolidJS 和反应性,我们可以自己编写 signal。
18 |
19 | 然后,文章详细地讲解了如何创建自己的
20 | signal。基本的反应性系统需要两个原语:`createSignal` 和
21 | `createEffect`。`createSignal` 用于读取和设置反应性值,`createEffect`
22 | 用于在该值更改时运行副作用。在 `createSignal` 中,我们将初始信号值保存在 `value`
23 | 变量中,然后创建一个读取器函数 `read` 和一个写入器函数 `write`。在
24 | `createEffect`
25 | 中,我们设置当前监听器为回调函数,并在调用回调函数时运行它。通过在 signal
26 | 的读取器函数中追踪订阅者并在写入器函数中调用订阅者函数,我们实现了反应性的效果。
27 |
28 | 最后,文章展示了如何使用我们自己创建的
29 | signal,以及如何用它们创建简单的计数器。我们可以使用 `setCount`
30 | 来更新计数器,并使用 effect 将计数器的值设置为
31 | `button.innerText`。整个过程展示了如何编写自己的 signal
32 | 和如何在实际应用中使用它们。
33 |
34 | ## 代码
35 |
36 | 代码在 [./signal.mjs](./signal.mjs) 文件。
37 |
38 | ### createSignal
39 |
40 | `createSignal`
41 | 函数用于创建一个响应式变量。这个函数接受一个初始值,返回一个数组。
42 |
43 | - 数组的第一个元素是一个读取该变量值的函数
44 | - 第二个元素是一个设置该变量值的函数。
45 |
46 | 这个数组可以用解构赋值语法来获取这两个函数。这个函数可以用于创建一些响应式状态,比如表单的输入值,或者一些
47 | UI 的状态。
48 |
49 | 当这个响应式变量的值被修改时,它会通知所有依赖于它的副作用函数。
50 |
51 | ### createEffect
52 |
53 | `createEffect`
54 | 函数用于创建一个副作用函数,也就是那些当响应式变量的值发生变化时需要执行的函数。
55 |
56 | 它接受一个函数作为参数,该函数即为需要执行的副作用函数。当副作用函数被创建时,它会自动运行一次。当与它关联的响应式变量的值发生变化时,副作用函数会被再次执行。
57 |
58 | 可以使用 `createEffect` 函数来创建一些需要自动响应数据变化的业务逻辑。
59 |
60 | ### 其他
61 |
62 | 这段代码还定义了一些类型别名 `ReadSignal`、`WriteSignal` 和
63 | `Signal`,用于定义这些函数所使用的类型。
64 |
65 | - `ReadSignal` 表示读取响应式变量的函数类型,
66 | - `WriteSignal` 表示设置响应式变量的函数类型,
67 | - `Signal` 表示一个响应式变量的类型,是一个由 `ReadSignal` 和 `WriteSignal`
68 | 组成的元组类型。
69 |
--------------------------------------------------------------------------------