├── .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}`; 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 | --------------------------------------------------------------------------------