subay 0.0.7 → 0.0.10

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.zh.md CHANGED
@@ -1,377 +1,682 @@
1
- # 🐣 Subay
2
-
3
- 一个 <700 LOC 的声明式、响应式 UI 库。
4
-
5
- ## ✨ 特点
6
-
7
- - **轻量小巧**: 不到700行代码,体积小,加载快
8
- - **声明式渲染**: 使用模板字面量进行声明式UI构建
9
- - **响应式状态**: 简单而强大的响应式状态管理
10
- - **高效更新**: 优化的DOM更新,最小化DOM操作
11
- - **TypeScript支持**: 完整的TypeScript类型定义
12
- - **零依赖**: 不依赖任何外部库
13
-
14
- ## 📦 安装
15
-
16
- ### NPM
17
-
18
- ```bash
19
- npm install subay
20
- ```
21
-
22
- ### Yarn
23
-
24
- ```bash
25
- yarn add subay
26
- ```
27
-
28
- ### 直接引入
29
-
30
- ```html
31
- <script type="module">
32
- import { html, o, S } from 'https://cdn.jsdelivr.net/npm/subay@latest/lib/index.js';
33
- // 使用 Subay
34
- </script>
35
- ```
36
-
37
- ## 🔧 基础用法
38
-
39
- ### 🔄 状态管理
40
-
41
- ```javascript
42
- import { o, S } from 'subay';
43
-
44
- // 创建可观察值
45
- const count = o(0);
46
-
47
- // 创建计算属性
48
- const doubled = S(() => count() * 2);
49
-
50
- // 读取值
51
- console.log(count()); // 输出: 0
52
- console.log(doubled()); // 输出: 0
53
-
54
- // 更新值
55
- count(5);
56
- console.log(count()); // 输出: 5
57
- console.log(doubled()); // 输出: 10
58
- ```
59
-
60
- ### 🎨 渲染
61
-
62
- ```javascript
63
- import { html, o } from 'subay';
64
-
65
- const name = o('世界');
66
- const count = o(0);
67
-
68
- document.body.append(
69
- ...html`
70
- <div>
71
- <h1>你好,${name}!</h1>
72
- <p>计数: ${count}</p>
73
- <button onclick="${() => count(count() + 1)}">增加</button>
74
- <button onclick="${() => name('Subay')}">更改名称</button>
75
- </div>
76
- `,
77
- );
78
- ```
79
-
80
- ### 📋 列表
81
-
82
- ```javascript
83
- import { html, o, map } from 'subay';
84
-
85
- const items = o(['苹果', '香蕉', '樱桃']);
86
-
87
- document.body.append(
88
- ...html`
89
- <ul>
90
- ${map(
91
- () => items(),
92
- (item) => html` <li>${item}</li> `,
93
- )}
94
- </ul>
95
- <button onclick="${() => items([...items(), '日期'])}">添加项目</button>
96
- `,
97
- );
98
- ```
99
-
100
- ## 📚 API
101
-
102
- ### ⚙️ 状态
103
-
104
- #### `o(value)`
105
-
106
- 创建一个可观察值。
107
-
108
- - **参数**: `value` - 初始值
109
- - **返回值**: 一个函数,调用时返回当前值,传入参数时更新值
110
-
111
- ```javascript
112
- const count = o(0);
113
- count(); // 读取值
114
- count(5); // 更新值
115
- ```
116
-
117
- #### `S(fn, initialValue?)`
118
-
119
- 创建一个计算属性。
120
-
121
- - **参数**:
122
- - `fn` - 计算函数,接收前一个值作为参数
123
- - `initialValue` - 可选的初始值
124
- - **返回值**: 一个函数,调用时返回计算结果
125
-
126
- ```javascript
127
- const doubled = S(() => count() * 2);
128
- doubled(); // 读取计算结果
129
- ```
130
-
131
- #### `root(fn)`
132
-
133
- 创建一个根上下文,用于管理计算属性的生命周期。
134
-
135
- - **参数**: `fn` - 函数,接收一个销毁函数作为参数
136
- - **返回值**: 函数执行的结果
137
-
138
- ```javascript
139
- root((destroy) => {
140
- const count = o(0);
141
- const doubled = S(() => count() * 2);
142
-
143
- // 使用 count 和 doubled
144
-
145
- // 可选:在适当的时候调用 destroy() 清理资源
146
- });
147
- ```
148
-
149
- #### `cleanup(fn)`
150
-
151
- 注册一个清理函数,在计算属性重新计算或销毁时执行。
152
-
153
- - **参数**: `fn` - 清理函数
154
- - **返回值**: 传入的清理函数
155
-
156
- ```javascript
157
- S(() => {
158
- const timer = setInterval(() => {
159
- console.log('tick');
160
- }, 1000);
161
-
162
- cleanup(() => clearInterval(timer));
163
-
164
- return count();
165
- });
166
- ```
167
-
168
- #### `transaction(fn)`
169
-
170
- 批量执行多个更新,减少不必要的计算和渲染。
171
-
172
- - **参数**: `fn` - 包含更新操作的函数
173
- - **返回值**: 函数执行的结果
174
-
175
- ```javascript
176
- transaction(() => {
177
- count(1);
178
- name('Subay');
179
- items(['a', 'b', 'c']);
180
- });
181
- ```
182
-
183
- ### 🖼️ 渲染
184
-
185
- #### `html`
186
-
187
- 用于创建HTML元素的模板字面量标签。
188
-
189
- - **用法**: `html\`...\``
190
- - **返回值**: 一个DOM节点数组
191
-
192
- ```javascript
193
- const element = html`
194
- <div class="container">
195
- <h1>Hello ${name}!</h1>
196
- </div>
197
- `;
198
- document.body.append(...element);
199
- ```
200
-
201
- #### `svg`
202
-
203
- 用于创建SVG元素的模板字面量标签。
204
-
205
- - **用法**: `svg\`...\``
206
- - **返回值**: 一个DOM节点数组
207
-
208
- ```javascript
209
- const icon = svg /*html*/ `
210
- <svg width="24" height="24" viewBox="0 0 24 24">
211
- <path d="M12 2L2 7l10 5 10-5-10-5z"/>
212
- </svg>
213
- `;
214
- document.body.append(...icon);
215
- ```
216
-
217
- #### `map(arrayFn, itemFn)`
218
-
219
- 用于渲染列表的函数。
220
-
221
- - **参数**:
222
- - `arrayFn` - 返回数组的函数
223
- - `itemFn` - 为每个数组项创建DOM节点的函数
224
- - **返回值**: 一个DOM节点数组
225
-
226
- ```javascript
227
- const list = map(
228
- () => items(),
229
- (item, index) => html` <li key="${index}">${item}</li> `,
230
- );
231
- ```
232
-
233
- ### 🛠️ 工具
234
-
235
- #### `isObservable(value)`
236
-
237
- 检查一个值是否是可观察值。
238
-
239
- - **参数**: `value` - 要检查的值
240
- - **返回值**: 布尔值
241
-
242
- #### `isComputation(value)`
243
-
244
- 检查一个值是否是计算属性。
245
-
246
- - **参数**: `value` - 要检查的值
247
- - **返回值**: 布尔值
248
-
249
- #### `isReactive(value)`
250
-
251
- 检查一个值是否是响应式的(可观察值或计算属性)。
252
-
253
- - **参数**: `value` - 要检查的值
254
- - **返回值**: 布尔值
255
-
256
- #### `sample(fn)`
257
-
258
- 运行一个函数,但不追踪其依赖。
259
-
260
- - **参数**: `fn` - 要运行的函数
261
- - **返回值**: 函数执行的结果
262
-
263
- ```javascript
264
- const value = sample(() => count()); // 读取count的值但不建立依赖关系
265
- ```
266
-
267
- ## ⚠️ 注意事项 (Caveats)
268
-
269
- ### 1. 标签位置的插值
270
-
271
- 在模板字符串中,如果标签位置使用插值,插值应该是一个字符串或者一个返回字符串的函数,返回的字符串是 HTML/SVG 元素的名字。如果需要使用组件,需要直接调用方法。
272
-
273
- **正确用法**:
274
-
275
- ```javascript
276
- // 静态标签
277
- html`<div>Hello</div>`;
278
-
279
- // 动态标签(使用字符串)
280
- const tagName = o('div');
281
- html`<${tagName}>Hello</${tagName}>`;
282
-
283
- // 动态标签(使用返回字符串的函数)
284
- const getTagName = () => 'div';
285
- html`<${getTagName}>Hello</${getTagName}>`;
286
-
287
- // 使用组件(直接调用方法)
288
- const MyComponent = () => html`<div>Component</div>`;
289
- html`<div>${MyComponent()}</div>`;
290
- ```
291
-
292
- **错误用法**:
293
-
294
- ```javascript
295
- // 错误:组件没有直接调用
296
- const MyComponent = () => html`<div>Component</div>`;
297
- html`<${MyComponent}>Hello</${MyComponent}>`; // 这会尝试创建一个名为 "function MyComponent() {...}" 的元素
298
- ```
299
-
300
- ### 2. 动态属性值
301
-
302
- 如果需要动态设置 HTML/SVG 元素的属性值,需要传入 `S()` 或 `o()` 的返回值。如果直接传入普通的函数,这个函数将会被当做属性值,而不是函数的返回值。
303
-
304
- **正确用法**:
305
-
306
- ```javascript
307
- // 使用可观察值
308
- const disabled = o(false);
309
- html`<button disabled="${disabled}">Click</button>`;
310
-
311
- // 使用计算属性
312
- const isDisabled = S(() => count() > 5);
313
- html`<button disabled="${isDisabled}">Click</button>`;
314
-
315
- // 事件绑定(直接传入函数)
316
- html`<button onclick="${() => count(count() + 1)}">Increment</button>`;
317
- ```
318
-
319
- **错误用法**:
320
-
321
- ```javascript
322
- // 错误:普通函数被当做属性值
323
- const getDisabled = () => false;
324
- html`<button disabled="${getDisabled}">Click</button>`; // 这会将 "function getDisabled() {...}" 作为属性值
325
- ```
326
-
327
- ## 💡 动机
328
-
329
- Subay 的创建动机是提供一个轻量级、简单易用的响应式UI库,避免大型框架的复杂性和体积。它的设计理念是:
330
-
331
- - **简洁至上**: 保持API简单直观,易于学习和使用
332
- - **性能优先**: 优化DOM更新,减少不必要的计算
333
- - **最小化依赖**: 不依赖任何外部库,保持体积小
334
- - **TypeScript支持**: 提供完整的类型定义,提高开发体验
335
-
336
- ### 与 Sinuous 的关系
337
-
338
- Subay 参考了 [Sinuous](https://github.com/luwes/sinuous) 库的设计思想,`o`、`S` 等API几乎与 Sinuous 保持一致,但在实现细节上有以下不同:
339
-
340
- 1. **S 的执行时机**: 在 Subay 中,即使某个 S 函数的结果不会被使用,它也会被执行。而在 Sinuous 中,只有当 S 函数的结果被实际使用时,才会执行该函数。
341
-
342
- **示例**:
343
-
344
- ```javascript
345
- const userId = o('');
346
- const defaultThemeId = o('rainbow');
347
-
348
- const themeId = S(() => {
349
- if (!userId()) {
350
- return defaultThemeId();
351
- }
352
- return userTheme();
353
- });
354
- const userTheme = S(() => {
355
- // if !userId(), sinuous 不会执行这里
356
- // 但 subay 会执行
357
- console.log('side effect on ', userId());
358
- return `${userId()}'s theme`;
359
- });
360
- S(() => {
361
- console.log('now the theme is ', themeId());
362
- });
363
-
364
- userId('John');
365
- userId('');
366
- userId('Mary');
367
- ```
368
-
369
- 在这个例子中,当 `userId()` 为空字符串时,`themeId()` 会返回 `defaultThemeId()`,而不会使用 `userTheme()` 的结果。在 Sinuous 中,`userTheme()` 函数不会被执行;但在 Subay 中,`userTheme()` 函数仍然会被执行,产生副作用。
370
-
371
- 2. **o 对 S 的引用管理**: 当 S 重新计算时,上次计算时 S 依赖的 o 们不会马上忘记 S,而是等到下次 o 被更新时才过滤掉已经被「忘记」的 S。由于 o 更新的频率通常比 S 重新计算的频率低,这种方式轻微提升了性能。
372
-
373
- Subay 适合那些需要轻量级UI解决方案的项目,特别是小型应用、原型开发或对性能有较高要求的场景。
374
-
375
- ## 📄 License
376
-
377
- MIT License - 详见 [LICENSE](LICENSE) 文件。
1
+ # Subay
2
+
3
+ Subay 是一个轻量级的响应式编程库,提供了状态管理和 DOM 操作的功能。
4
+
5
+ ## API
6
+
7
+ ### root
8
+
9
+ **简介**
10
+ 创建一个根更新上下文,用于管理响应式状态的生命周期。
11
+
12
+ **语法**
13
+
14
+ ```typescript
15
+ root<T>(fn: (destroy: () => void) => T): T
16
+ ```
17
+
18
+ **参数**
19
+
20
+ - `fn`: 一个函数,接收一个 `destroy` 函数作为参数,用于手动销毁根上下文。
21
+
22
+ **返回值**
23
+
24
+ - `T`: `fn` 函数的返回值。
25
+
26
+ **例子**
27
+
28
+ ```typescript
29
+ import { root, o, S } from 'subay';
30
+
31
+ root((destroy) => {
32
+ const count = o(0);
33
+ const doubled = S(() => count() * 2);
34
+
35
+ console.log(doubled()); // 0
36
+ count(1);
37
+ console.log(doubled()); // 2
38
+
39
+ // 手动销毁
40
+ destroy();
41
+ });
42
+ ```
43
+
44
+ **注意**
45
+
46
+ 销毁使用 `root` 创建的上下文的唯一方式就是调用 `destroy` 回调。
47
+
48
+ ### S
49
+
50
+ **简介**
51
+ 创建一个计算值,当依赖的可观察值变化时自动重新计算。
52
+
53
+ **语法**
54
+
55
+ ```typescript
56
+ S<T>(fn: (pv: T | undefined) => T, value?: T): IS<T>
57
+ ```
58
+
59
+ **参数**
60
+
61
+ - `fn`: 计算函数,接收上一次的计算值作为参数。
62
+ - `value`: 可选的初始值。
63
+
64
+ **返回值**
65
+
66
+ - `IS<T>`: 一个函数,调用时返回计算值。
67
+
68
+ **例子**
69
+
70
+ ```typescript
71
+ import { S, o } from 'subay';
72
+
73
+ const count = o(0);
74
+ const doubled = S(() => count() * 2);
75
+
76
+ console.log(doubled()); // 0
77
+ count(1);
78
+ console.log(doubled()); // 2
79
+ ```
80
+
81
+ ### o / observable
82
+
83
+ **简介**
84
+ 创建一个可观察值,用于存储和更新状态。
85
+
86
+ **语法**
87
+
88
+ ```typescript
89
+ o<T>(value: T): IO<T>
90
+ ```
91
+
92
+ **参数**
93
+
94
+ - `value`: 初始值。
95
+
96
+ **返回值**
97
+
98
+ - `IO<T>`: 一个函数,无参数调用时返回当前值,带参数调用时更新值并返回新值。
99
+
100
+ **例子**
101
+
102
+ ```typescript
103
+ import { o } from 'subay';
104
+
105
+ const count = o(0);
106
+ console.log(count()); // 0
107
+ count(1);
108
+ console.log(count()); // 1
109
+ ```
110
+
111
+ ### cleanup
112
+
113
+ **简介**
114
+ 注册一个清理函数,当当前更新上下文被销毁时执行。
115
+
116
+ **语法**
117
+
118
+ ```typescript
119
+ cleanup<T extends () => void>(f: T): T
120
+ ```
121
+
122
+ **参数**
123
+
124
+ - `f`: 清理函数。
125
+
126
+ **返回值**
127
+
128
+ - `T`: 传入的清理函数。
129
+
130
+ **例子**
131
+
132
+ ```typescript
133
+ import { root, S, o, cleanup } from 'subay';
134
+
135
+ root(() => {
136
+ const count = o(0);
137
+
138
+ S(() => {
139
+ console.log(count());
140
+ cleanup(() => {
141
+ console.log('Cleanup called');
142
+ });
143
+ });
144
+
145
+ count(1);
146
+ });
147
+ // 输出: 0, 1, Cleanup called
148
+ ```
149
+
150
+ ### transaction
151
+
152
+ **简介**
153
+ 创建一个事务,批量更新可观察值,避免中间状态的计算。
154
+
155
+ **语法**
156
+
157
+ ```typescript
158
+ transaction<T>(f: () => T): T
159
+ ```
160
+
161
+ **参数**
162
+
163
+ - `f`: 事务函数,在其中可以更新多个可观察值。
164
+
165
+ **返回值**
166
+
167
+ - `T`: `f` 函数的返回值。
168
+
169
+ **例子**
170
+
171
+ ```typescript
172
+ import { transaction, o, S } from 'subay';
173
+
174
+ const a = o(1);
175
+ const b = o(2);
176
+ const sum = S(() => a() + b());
177
+
178
+ S(() => console.log(sum())); // 3
179
+
180
+ transaction(() => {
181
+ a(2);
182
+ b(3);
183
+ });
184
+ // 输出: 5 (只计算一次)
185
+ ```
186
+
187
+ ### sample
188
+
189
+ **简介**
190
+ 在不建立依赖关系的情况下获取可观察值的值。
191
+
192
+ **语法**
193
+
194
+ ```typescript
195
+ sample<T>(f: () => T): T
196
+ ```
197
+
198
+ **参数**
199
+
200
+ - `f`: 函数,在其中可以访问可观察值但不会建立依赖关系。
201
+
202
+ **返回值**
203
+
204
+ - `T`: `f` 函数的返回值。
205
+
206
+ **例子**
207
+
208
+ ```typescript
209
+ import { sample, o, S } from 'subay';
210
+
211
+ const count = o(0);
212
+ let cachedValue;
213
+
214
+ S(() => {
215
+ // 不建立依赖关系
216
+ cachedValue = sample(() => count());
217
+ console.log(cachedValue);
218
+ });
219
+
220
+ count(1); // 不会触发重新计算
221
+ console.log(cachedValue); // 0
222
+ ```
223
+
224
+ ### isListening
225
+
226
+ **简介**
227
+ 检查当前是否处于响应式监听状态。
228
+
229
+ **语法**
230
+
231
+ ```typescript
232
+ isListening(): boolean
233
+ ```
234
+
235
+ **参数**
236
+
237
+ - 无
238
+
239
+ **返回值**
240
+
241
+ - `boolean`: 当前是否处于响应式监听状态。
242
+
243
+ **例子**
244
+
245
+ ```typescript
246
+ import { isListening, S } from 'subay';
247
+
248
+ console.log(isListening()); // false
249
+
250
+ S(() => {
251
+ console.log(isListening()); // true
252
+ });
253
+ ```
254
+
255
+ ### subscribe
256
+
257
+ **简介**
258
+ 订阅一个函数,当依赖的可观察值变化时自动执行。
259
+
260
+ **语法**
261
+
262
+ ```typescript
263
+ subscribe(f: () => void): () => void
264
+ ```
265
+
266
+ **参数**
267
+
268
+ - `f`: 订阅函数。
269
+
270
+ **返回值**
271
+
272
+ - `() => void`: 取消订阅的函数。
273
+
274
+ **例子**
275
+
276
+ ```typescript
277
+ import { subscribe, o } from 'subay';
278
+
279
+ const count = o(0);
280
+ const unsubscribe = subscribe(() => {
281
+ console.log(count());
282
+ });
283
+
284
+ count(1); // 输出: 1
285
+ count(2); // 输出: 2
286
+
287
+ unsubscribe();
288
+ count(3); // 无输出
289
+ ```
290
+
291
+ ### unsubscribe
292
+
293
+ **简介**
294
+ 取消订阅一个函数。
295
+
296
+ **语法**
297
+
298
+ ```typescript
299
+ unsubscribe(f: () => void): void
300
+ ```
301
+
302
+ **参数**
303
+
304
+ - `f`: 要取消订阅的函数。
305
+
306
+ **返回值**
307
+
308
+ -
309
+
310
+ **例子**
311
+
312
+ ```typescript
313
+ import { subscribe, unsubscribe, o } from 'subay';
314
+
315
+ const count = o(0);
316
+ const fn = () => console.log(count());
317
+
318
+ subscribe(fn);
319
+ count(1); // 输出: 1
320
+
321
+ unsubscribe(fn);
322
+ count(2); // 无输出
323
+ ```
324
+
325
+ ### isObservable
326
+
327
+ **简介**
328
+ 检查一个值是否是可观察值。
329
+
330
+ **语法**
331
+
332
+ ```typescript
333
+ isObservable(o: any): o is IO<any>
334
+ ```
335
+
336
+ **参数**
337
+
338
+ - `o`: 要检查的值。
339
+
340
+ **返回值**
341
+
342
+ - `boolean`: 是否是可观察值。
343
+
344
+ **例子**
345
+
346
+ ```typescript
347
+ import { isObservable, o, S } from 'subay';
348
+
349
+ const count = o(0);
350
+ const doubled = S(() => count() * 2);
351
+
352
+ console.log(isObservable(count)); // true
353
+ console.log(isObservable(doubled)); // false
354
+ ```
355
+
356
+ ### isComputed
357
+
358
+ **简介**
359
+ 检查一个值是否是计算值。
360
+
361
+ **语法**
362
+
363
+ ```typescript
364
+ isComputed(o: any): o is IS<any>
365
+ ```
366
+
367
+ **参数**
368
+
369
+ - `o`: 要检查的值。
370
+
371
+ **返回值**
372
+
373
+ - `boolean`: 是否是计算值。
374
+
375
+ **例子**
376
+
377
+ ```typescript
378
+ import { isComputed, o, S } from 'subay';
379
+
380
+ const count = o(0);
381
+ const doubled = S(() => count() * 2);
382
+
383
+ console.log(isComputed(count)); // false
384
+ console.log(isComputed(doubled)); // true
385
+ ```
386
+
387
+ ### isReactive
388
+
389
+ **简介**
390
+ 检查一个值是否是响应式值(可观察值或计算值)。
391
+
392
+ **语法**
393
+
394
+ ```typescript
395
+ isReactive(o: any): o is IO<any> | IS<any>
396
+ ```
397
+
398
+ **参数**
399
+
400
+ - `o`: 要检查的值。
401
+
402
+ **返回值**
403
+
404
+ - `boolean`: 是否是响应式值。
405
+
406
+ **例子**
407
+
408
+ ```typescript
409
+ import { isReactive, o, S } from 'subay';
410
+
411
+ const count = o(0);
412
+ const doubled = S(() => count() * 2);
413
+
414
+ console.log(isReactive(count)); // true
415
+ console.log(isReactive(doubled)); // true
416
+ console.log(isReactive(42)); // false
417
+ ```
418
+
419
+ ### map
420
+
421
+ **简介**
422
+ 将一个数组映射为 DOM 节点数组,当数组变化时自动更新。
423
+
424
+ **语法**
425
+
426
+ ```typescript
427
+ map<T>(array: () => T[], f: (item: T) => Node[]): Node[]
428
+ ```
429
+
430
+ **参数**
431
+
432
+ - `array`: 一个返回数组的函数。
433
+ - `f`: 映射函数,将数组项转换为 DOM 节点数组。
434
+
435
+ **返回值**
436
+
437
+ - `Node[]`: DOM 节点数组。
438
+
439
+ **例子**
440
+
441
+ ```typescript
442
+ import { map, o, html } from 'subay';
443
+
444
+ const items = o(['a', 'b', 'c']);
445
+ const list = map(
446
+ () => items(),
447
+ (item) => html`<li>${item}</li>`,
448
+ );
449
+
450
+ document.body.append(...list);
451
+ // 渲染: <li>a</li><li>b</li><li>c</li>
452
+
453
+ items(['a', 'b', 'c', 'd']);
454
+ // 自动更新: <li>a</li><li>b</li><li>c</li><li>d</li>
455
+ ```
456
+
457
+ ### svg
458
+
459
+ **简介**
460
+ 创建 SVG 元素的模板标签函数。
461
+
462
+ **语法**
463
+
464
+ ```typescript
465
+ svg(segments: TemplateStringsArray, ...values: any[]): Node[]
466
+ ```
467
+
468
+ **参数**
469
+
470
+ - `segments`: 模板字符串的静态部分。
471
+ - `values`: 模板字符串的动态部分。
472
+
473
+ **返回值**
474
+
475
+ - `Node[]`: SVG 元素节点数组。
476
+
477
+ **例子**
478
+
479
+ ```typescript
480
+ import { svg, o } from 'subay';
481
+
482
+ const radius = o(50);
483
+ const circle = svg`<circle cx="100" cy="100" r="${radius}" fill="red"/>`;
484
+
485
+ document.getElementById('svg').append(...circle);
486
+ ```
487
+
488
+ ### html
489
+
490
+ **简介**
491
+ 创建 HTML 元素的模板标签函数。
492
+
493
+ **语法**
494
+
495
+ ```typescript
496
+ html(segments: TemplateStringsArray, ...values: any[]): Node[]
497
+ ```
498
+
499
+ **参数**
500
+
501
+ - `segments`: 模板字符串的静态部分。
502
+ - `values`: 模板字符串的动态部分。
503
+
504
+ **返回值**
505
+
506
+ - `Node[]`: HTML 元素节点数组。
507
+
508
+ **例子**
509
+
510
+ ```typescript
511
+ import { html, o } from 'subay';
512
+
513
+ const count = o(0);
514
+ const counter = html`
515
+ <div>
516
+ <p>Count: ${count}</p>
517
+ <button onclick=${() => count(count() + 1)}>Increment</button>
518
+ </div>
519
+ `;
520
+
521
+ document.body.append(...counter);
522
+ // 点击按钮会自动更新 count 的值
523
+ ```
524
+
525
+ **使用 HTML**
526
+
527
+ Subay 使用 `DOMParser` 解析 HTML 模板, `html` 的参数必须是合法的 HTML 字符串,(同理, `svg` 的参数必须是合法的 SVG 字符串), 这是有别于 JSX 的地方。
528
+
529
+ **例子**
530
+
531
+ ```typescript
532
+ import { html } from 'subay';
533
+
534
+ // 合法的 HTML 字符串
535
+ const validHtml = html`<div><p>Hello</p></div>`;
536
+
537
+ document.body.append(...validHtml);
538
+ ```
539
+
540
+ **错误示例**
541
+
542
+ ```typescript
543
+ import { html } from 'subay';
544
+
545
+ // 自闭合标签
546
+ const invalidHtml = html`<div>
547
+ <span />
548
+ <span />
549
+ </div>`; // 会导致解析成 <div><span><span></span></span></div>
550
+ ```
551
+
552
+ **传入对象作为元素属性**
553
+
554
+ 如果需要传入一个对象作为元素属性,可以使用 `...` 语法。
555
+
556
+ **语法**
557
+
558
+ ```typescript
559
+ html`<tag ...${props}></tag>`;
560
+ ```
561
+
562
+ **例子**
563
+
564
+ ```typescript
565
+ import { html, o } from 'subay';
566
+
567
+ const props = o({
568
+ className: 'container',
569
+ style: 'color: red;',
570
+ });
571
+
572
+ const element = html`<div ...${props}></div>`;
573
+
574
+ document.body.append(...element);
575
+ ```
576
+
577
+ **组件的声明**
578
+
579
+ 组件是一个返回 Node\[] 的函数,可以接收参数。和 React、Vue 等不同,组件的参数是一个列表。
580
+
581
+ **语法**
582
+
583
+ ```typescript
584
+ const Component = (text, children: () => Node[]) => {
585
+ return html`<div>${text}</div>`;
586
+ };
587
+ ```
588
+
589
+ **例子**
590
+
591
+ ```typescript
592
+ import { html } from 'subay';
593
+
594
+ const Greeting = (name: string, children: () => Node[]) => {
595
+ return html`
596
+ <div>
597
+ <h1>Hello, ${name}!</h1>
598
+ ${children()}
599
+ </div>
600
+ `;
601
+ };
602
+ ```
603
+
604
+ **组件的使用**
605
+
606
+ 在模板中使用组件时,在标签处引用组件。
607
+ 因为模板是 HTML 字符串,引用的时候不支持自闭合标签。
608
+ 传给组件的参数必须和组件声明的参数顺序一致。
609
+ 组件获得的参数总是和传入的保持一致。
610
+
611
+ **例子**
612
+
613
+ ```typescript
614
+ import { html, o } from 'subay';
615
+
616
+ const Greeting = (greet: string, name: () => string) => {
617
+ return html`<h1>${greet}, ${name}!</h1>`;
618
+ };
619
+
620
+ const name = o('World');
621
+ const app = html` <div><${Greeting} greet=${'Hello'} name=${name}></${Greeting}></div> `;
622
+
623
+ document.body.append(...app);
624
+ ```
625
+
626
+ **错误示例**
627
+
628
+ ```typescript
629
+ import { html, o } from 'subay';
630
+
631
+ const Greeting = (greet: string, name: () => string) => {
632
+ return html`<h1>${greet}, ${name}!</h1>`;
633
+ };
634
+
635
+ const name = o('World');
636
+ // 错误, 后面的 span 会被当做 Greeting 的子节点。
637
+ const app = html`
638
+ <div>
639
+ <${Greeting}
640
+ greet=${'Hello'}
641
+ name=${name}
642
+ />
643
+ <span></span>
644
+ </div>
645
+ `;
646
+ // 错误, 参数顺序和 Greeting 的参数不一致
647
+ const app = html` <div><${Greeting} name=${name} greet=${'Hi'} ></${Greeting}></div> `;
648
+
649
+ document.body.append(...app);
650
+ ```
651
+
652
+ **动态的组件**
653
+
654
+ 标签位置的插值可以是 `S` 或 `o` 的返回值,可以根据条件动态选择要渲染的组件。
655
+
656
+ **例子**
657
+
658
+ ```typescript
659
+ import { html, o, S } from 'subay';
660
+
661
+ const Greeting = (name: () => string) => {
662
+ return html`<h1>Hello, ${name}!</h1>`;
663
+ };
664
+
665
+ const Farewell = (name: () => string) => {
666
+ return html`<h1>Goodbye, ${name}!</h1>`;
667
+ };
668
+
669
+ const showGreeting = o(true);
670
+ const name = o('World');
671
+ const Component = S(() => (showGreeting() ? Greeting : Farewell));
672
+
673
+ const app = html`
674
+ <div>
675
+ <${Component} name=${name} ></${Component}>
676
+ <button onclick=${() => showGreeting(!showGreeting())}>Toggle</button>
677
+ </div>
678
+ `;
679
+
680
+ document.body.append(...app);
681
+ ```
682
+