Skip to content

Commit 000ddf9

Browse files
committed
refactor(components): 正确处理 render 返回的 dispose 方法
1 parent f34ca51 commit 000ddf9

8 files changed

Lines changed: 263 additions & 202 deletions

File tree

apps/docs/src/components/demo/code/api.zh-Hans.json

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@
4646
"type": "BundledTheme | undefined",
4747
"reactive": true
4848
},
49+
{
50+
"name": "decorates",
51+
"summary": "装饰器名称\n\n",
52+
"remarks": "该值可由\n`getDecorates`\n获取。\n\n",
53+
"type": "string[] | undefined"
54+
},
4955
{
5056
"name": "palette",
5157
"summary": "指定当前组件采用的色盘\n\n",
@@ -97,7 +103,7 @@
97103
{
98104
"name": "html",
99105
"summary": "高亮代码\n\n",
100-
"type": "html(code: string, lang: BundledLanguage, ln?: number, wrap?: boolean, cls?: string, style?: BaseProps['style'], theme?: BundledTheme): string",
106+
"type": "html(code: string, lang: BundledLanguage, ln?: number, wrap?: boolean, cls?: string, style?: BaseProps['style'], theme?: BundledTheme, decorate?: string): string",
101107
"params": [
102108
{
103109
"name": "code",
@@ -133,6 +139,11 @@
133139
"name": "theme",
134140
"summary": "主题名称。可以为空,表示使用默认主题,默认主题会根据整个框架的主题而变化;\n\n",
135141
"type": "BundledTheme | undefined"
142+
},
143+
{
144+
"name": "decorate",
145+
"summary": "装饰器名称,仅作记录,需要后续调用 withDecorate 才能在内容上有所显示,如果要指定多个,可以使用半角逗号分隔;\n\n",
146+
"type": "string | undefined"
136147
}
137148
],
138149
"return": {
@@ -180,7 +191,7 @@
180191
"name": "Code.highlight",
181192
"summary": "高亮代码\n\n",
182193
"remarks": "用户需要自己在 package.json 的 dependencies 中导入\n[shiki](https://shiki.tmrs.site/) 该包才有高亮功能。\n\n",
183-
"type": "declare function highlight(code: string, lang?: BundledLanguage, ln?: number, wrap?: boolean, cls?: string, style?: BaseProps['style'], theme?: BundledTheme): Promise<string>",
194+
"type": "declare function highlight(code: string, lang?: BundledLanguage, ln?: number, wrap?: boolean, cls?: string, style?: BaseProps['style'], theme?: BundledTheme, decorate?: string): Promise<string>",
184195
"params": [
185196
{
186197
"name": "code",
@@ -216,12 +227,66 @@
216227
"name": "theme",
217228
"summary": "主题名称。可以为空,表示使用默认主题,默认主题会根据整个框架的主题而变化;\n\n",
218229
"type": "BundledTheme | undefined"
230+
},
231+
{
232+
"name": "decorate",
233+
"summary": "装饰器名称,仅作记录,需要后续调用 withDecorate 才能在内容上有所显示,如果要指定多个,可以使用半角逗号分隔;\n\n",
234+
"type": "string | undefined"
219235
}
220236
],
221237
"return": {
222238
"summary": "高亮后的 HTML 代码;\n\n",
223239
"type": "Promise<string>"
224240
},
225241
"pkg": "@cmfx/components"
242+
},
243+
{
244+
"kind": "function",
245+
"name": "Code.decorates",
246+
"summary": "获取装饰器的名称列表\n\n",
247+
"type": "declare function getDecorates(): Array<string>",
248+
"return": {
249+
"type": "string[]"
250+
},
251+
"pkg": "@cmfx/components"
252+
},
253+
{
254+
"kind": "function",
255+
"name": "Code.registerDecorate",
256+
"summary": "注册装饰器\n\n",
257+
"type": "declare function registerDecorate(name: string, decorate: Decorate): void",
258+
"params": [
259+
{
260+
"name": "name",
261+
"summary": "装饰器的名称;\n\n",
262+
"type": "string"
263+
},
264+
{
265+
"name": "decorate",
266+
"summary": "装饰器的实例;\n\n",
267+
"type": "Decorate"
268+
}
269+
],
270+
"return": {
271+
"type": "void"
272+
},
273+
"pkg": "@cmfx/components"
274+
},
275+
{
276+
"kind": "function",
277+
"name": "Code.withDecorate",
278+
"summary": "为 elem 及其子元素中的所有 shiki 代码块添加指定的组件\n\n",
279+
"remarks": "装饰器根据\n`highlight`\n的 decorate 参数而定。\n此操作会改变上下文环境,如果需要使用 Context 的相关信息,请使用 runWithOwner 创建上下文环境。\n\n",
280+
"type": "declare function withDecorate(elem: HTMLElement): void",
281+
"params": [
282+
{
283+
"name": "elem",
284+
"type": "HTMLElement"
285+
}
286+
],
287+
"return": {
288+
"type": "void"
289+
},
290+
"pkg": "@cmfx/components"
226291
}
227292
]

packages/components/src/button/button/root.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,7 @@ export function Root(props: Props) {
102102
let ref: HTMLElement;
103103

104104
if (props.hotkey && props.onclick) {
105-
onMount(() => {
106-
Hotkey.bind(props.hotkey!, () => {
107-
ref!.click();
108-
});
109-
});
105+
onMount(() => Hotkey.bind(props.hotkey!, () => ref!.click()));
110106
onCleanup(() => Hotkey.unbind(props.hotkey!));
111107
}
112108

packages/components/src/code/decorate.tsx

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,41 +52,50 @@ export function getDecorates(): Array<string> {
5252
}
5353

5454
/**
55-
* 为 elem 及其子元素中的所有 shiki 代码块添加指定的组件
55+
* 为 elem 及其子元素中的所有 shiki 代码块添加指定的装饰组件
5656
*
57+
* @param elem - 要装饰的元素;
58+
* @returns 装饰组件的注销方法;
5759
* @remarks
5860
* 装饰器根据 {@link highlight} 的 decorate 参数而定。
5961
* 此操作会改变上下文环境,如果需要使用 Context 的相关信息,请使用 runWithOwner 创建上下文环境。
6062
*/
61-
export function withDecorate(elem: HTMLElement): void {
63+
export function withDecorate(elem: HTMLElement): () => void {
64+
const disposes: Array<() => void> = [];
65+
6266
// 当前元素匹配
6367
if (elem.matches('[data-code]')) {
64-
mount(elem as HTMLPreElement);
65-
return;
68+
mount(elem as HTMLPreElement, disposes);
69+
} else {
70+
const elems = elem.querySelectorAll('[data-code]');
71+
for (const elem of elems) {
72+
mount(elem as HTMLPreElement, disposes);
73+
}
6674
}
6775

68-
const elems = elem.querySelectorAll('[data-code]');
69-
for (const elem of elems) {
70-
mount(elem as HTMLPreElement);
71-
}
76+
const cancel = () => {
77+
for (const d of disposes) {
78+
d();
79+
}
80+
};
81+
82+
return cancel;
7283
}
7384

7485
/**
7586
* 根据 pre 上的 data-decorate 属性挂载相应的装饰器
7687
*/
77-
function mount(pre: HTMLPreElement) {
88+
function mount(pre: HTMLPreElement, disposes: Array<() => void>): void {
7889
const names = pre.dataset.decorate;
79-
if (!names) {
80-
return;
81-
}
82-
83-
for (const name of names.split(',')) {
84-
if (!decorates.has(name)) {
85-
throw new Error(`装饰器 ${name} 不存在`);
90+
if (names) {
91+
for (const name of names.split(',')) {
92+
if (!decorates.has(name)) {
93+
throw new Error(`装饰器 ${name} 不存在`);
94+
}
95+
96+
const d = decorates.get(name)!;
97+
disposes.push(render(() => d(pre), pre));
8698
}
87-
88-
const d = decorates.get(name)!;
89-
render(() => d(pre), pre);
9099
}
91100
}
92101

packages/components/src/code/root.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// SPDX-License-Identifier: MIT
44

55
import type { BundledLanguage, BundledTheme } from 'shiki/bundle/full';
6-
import { createEffect, createSignal, getOwner, type JSX, runWithOwner } from 'solid-js';
6+
import { createEffect, createSignal, getOwner, type JSX, onCleanup, runWithOwner } from 'solid-js';
77
import { template } from 'solid-js/web';
88

99
import { type BaseProps, type BaseRef, joinClass, type RefProps } from '@components/base';
@@ -85,6 +85,20 @@ export function Root(props: Props): JSX.Element {
8585
const [html, setHTML] = createSignal<HTMLElement>();
8686
const owner = getOwner();
8787

88+
const disposes: Array<() => void> = [];
89+
const cancel = () => {
90+
if (disposes.length === 0) {
91+
return;
92+
}
93+
94+
for (const d of disposes) {
95+
d();
96+
}
97+
disposes.length = 0;
98+
};
99+
100+
onCleanup(() => cancel());
101+
88102
createEffect(async () => {
89103
const cls = joinClass(props.palette, props.class);
90104
const pre = await highlight(
@@ -107,6 +121,8 @@ export function Root(props: Props): JSX.Element {
107121
});
108122

109123
createEffect(() => {
124+
cancel();
125+
110126
const el = html();
111127
if (!el) {
112128
return;
@@ -121,7 +137,7 @@ export function Root(props: Props): JSX.Element {
121137
}
122138
});
123139

124-
runWithOwner(owner, () => withDecorate(el));
140+
runWithOwner(owner, () => disposes.push(withDecorate(el)));
125141
});
126142

127143
return <>{html()}</>;

packages/components/src/markdown/root.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import { Marked, type Token, type TokenizerAndRendererExtension } from 'marked';
66
import type { JSX, ValidComponent } from 'solid-js';
7-
import { createEffect, createSignal, getOwner, runWithOwner } from 'solid-js';
7+
import { createEffect, createSignal, getOwner, onCleanup, runWithOwner } from 'solid-js';
88
import { Dynamic, render } from 'solid-js/web';
99

1010
import type { BaseProps, BaseRef, RefProps } from '@components/base';
@@ -63,8 +63,24 @@ export function Root<T extends keyof HTMLElementTagNameMap = 'article'>(props: P
6363
const [html, setHTML] = createSignal(props.text);
6464
let ref: HTMLElement;
6565

66+
const disposes: Array<() => void> = [];
67+
const cancel = () => {
68+
if (disposes.length === 0) {
69+
return;
70+
}
71+
72+
for (const d of disposes) {
73+
d();
74+
}
75+
disposes.length = 0;
76+
};
77+
78+
onCleanup(() => cancel());
79+
6680
// 监视文本是否修改
6781
createEffect(async () => {
82+
cancel();
83+
6884
const ht = await p.parse(props.text || '', { async: true });
6985
setHTML(ht);
7086

@@ -79,7 +95,7 @@ export function Root<T extends keyof HTMLElementTagNameMap = 'article'>(props: P
7995

8096
Object.entries(props.components).forEach(([id, fn]) => {
8197
ref.querySelectorAll(`[data-markdown-component="${id}"]`)?.forEach(el => {
82-
runWithOwner(owner, () => render(fn, el));
98+
runWithOwner(owner, () => disposes.push(render(fn, el)));
8399
});
84100
});
85101

@@ -88,7 +104,7 @@ export function Root<T extends keyof HTMLElementTagNameMap = 'article'>(props: P
88104
}
89105

90106
if (ref) {
91-
runWithOwner(owner, () => Code.withDecorate(ref));
107+
runWithOwner(owner, () => disposes.push(Code.withDecorate(ref)));
92108
}
93109
});
94110
});

packages/core/src/hotkey/hotkey.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ describe('HotKey', () => {
1111
Hotkey.init(); // 多次调用
1212

1313
test('static', async () => {
14-
expect(Hotkey.hasKeys('f', 'control')).toBeFalsy();
14+
expect(Hotkey.hasKeys('f', 'control')).toBe(false);
1515

1616
Hotkey.bindKeys(() => {}, 'f', 'control');
17-
expect(Hotkey.hasKeys('f', 'control')).toBeTruthy();
17+
expect(Hotkey.hasKeys('f', 'control')).toBe(true);
1818

1919
expect(() => Hotkey.bindKeys(() => {}, 'f', 'control')).toThrow('快捷键 control+f 已经存在');
2020

2121
Hotkey.unbind(new Hotkey('F', 'control'));
22-
expect(Hotkey.hasKeys('f', 'control')).toBeFalsy();
22+
expect(Hotkey.hasKeys('f', 'control')).toBe(false);
2323
});
2424

2525
test('construct', () => {
@@ -37,9 +37,9 @@ describe('HotKey', () => {
3737
test('match', () => {
3838
const hk = new Hotkey('F', 'shift', 'meta');
3939

40-
expect(hk.match(new KeyboardEvent('keydown', { code: 'KeyF', shiftKey: true, metaKey: true }))).toBeTruthy();
40+
expect(hk.match(new KeyboardEvent('keydown', { code: 'KeyF', shiftKey: true, metaKey: true }))).toBe(true);
4141

42-
expect(hk.match(new KeyboardEvent('keydown', { code: 'KeyF', metaKey: true }))).toBeFalsy();
42+
expect(hk.match(new KeyboardEvent('keydown', { code: 'KeyF', metaKey: true }))).toBe(false);
4343
expect(
4444
hk.match(new KeyboardEvent('keydown', { code: 'KeyF', shiftKey: true, metaKey: true, altKey: true })),
4545
).toBeFalsy();

0 commit comments

Comments
 (0)