@mantou/gem 1.7.11 → 2.0.0-alpha.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/gem.es.js +1962 -1689
  4. package/dist/gem.umd.js +52 -54
  5. package/elements/base/dialog.d.ts +0 -11
  6. package/elements/base/dialog.d.ts.map +1 -1
  7. package/elements/base/dialog.js +93 -66
  8. package/elements/base/dialog.js.map +1 -1
  9. package/elements/base/gesture.d.ts +1 -2
  10. package/elements/base/gesture.d.ts.map +1 -1
  11. package/elements/base/gesture.js +157 -96
  12. package/elements/base/gesture.js.map +1 -1
  13. package/elements/base/link.d.ts +5 -5
  14. package/elements/base/link.d.ts.map +1 -1
  15. package/elements/base/link.js +249 -145
  16. package/elements/base/link.js.map +1 -1
  17. package/elements/base/modal-factory.d.ts +147 -139
  18. package/elements/base/modal-factory.d.ts.map +1 -1
  19. package/elements/base/modal-factory.js +84 -25
  20. package/elements/base/modal-factory.js.map +1 -1
  21. package/elements/base/reflect.d.ts +0 -1
  22. package/elements/base/reflect.d.ts.map +1 -1
  23. package/elements/base/reflect.js +83 -38
  24. package/elements/base/reflect.js.map +1 -1
  25. package/elements/base/route.d.ts +12 -24
  26. package/elements/base/route.d.ts.map +1 -1
  27. package/elements/base/route.js +263 -164
  28. package/elements/base/route.js.map +1 -1
  29. package/elements/base/title.d.ts +3 -11
  30. package/elements/base/title.d.ts.map +1 -1
  31. package/elements/base/title.js +110 -59
  32. package/elements/base/title.js.map +1 -1
  33. package/elements/base/unsafe.d.ts +0 -1
  34. package/elements/base/unsafe.d.ts.map +1 -1
  35. package/elements/base/unsafe.js +82 -33
  36. package/elements/base/unsafe.js.map +1 -1
  37. package/elements/base/use.d.ts.map +1 -1
  38. package/elements/base/use.js +95 -47
  39. package/elements/base/use.js.map +1 -1
  40. package/helper/i18n.d.ts +4 -9
  41. package/helper/i18n.d.ts.map +1 -1
  42. package/helper/i18n.js +69 -65
  43. package/helper/i18n.js.map +1 -1
  44. package/helper/logger.js +10 -9
  45. package/helper/logger.js.map +1 -1
  46. package/helper/react-shim.d.ts +15 -0
  47. package/helper/react-shim.d.ts.map +1 -0
  48. package/helper/react-shim.js +56 -0
  49. package/helper/react-shim.js.map +1 -0
  50. package/helper/react-utils.d.ts +15 -0
  51. package/helper/react-utils.d.ts.map +1 -0
  52. package/helper/react-utils.js +50 -0
  53. package/helper/react-utils.js.map +1 -0
  54. package/helper/request.d.ts +17 -7
  55. package/helper/request.d.ts.map +1 -1
  56. package/helper/request.js +103 -53
  57. package/helper/request.js.map +1 -1
  58. package/helper/ssr-shim.d.ts.map +1 -1
  59. package/helper/ssr-shim.js +8 -1
  60. package/helper/ssr-shim.js.map +1 -1
  61. package/helper/theme.d.ts +14 -21
  62. package/helper/theme.d.ts.map +1 -1
  63. package/helper/theme.js +55 -38
  64. package/helper/theme.js.map +1 -1
  65. package/index.d.ts +4 -3
  66. package/index.d.ts.map +1 -1
  67. package/index.js +5 -3
  68. package/index.js.map +1 -1
  69. package/lib/decorators.d.ts +74 -41
  70. package/lib/decorators.d.ts.map +1 -1
  71. package/lib/decorators.js +324 -84
  72. package/lib/decorators.js.map +1 -1
  73. package/lib/directives.d.ts +4 -0
  74. package/lib/directives.d.ts.map +1 -0
  75. package/lib/directives.js +5 -0
  76. package/lib/directives.js.map +1 -0
  77. package/lib/element.d.ts +112 -110
  78. package/lib/element.d.ts.map +1 -1
  79. package/lib/element.js +431 -470
  80. package/lib/element.js.map +1 -1
  81. package/lib/history.d.ts +4 -0
  82. package/lib/history.d.ts.map +1 -1
  83. package/lib/history.js +12 -13
  84. package/lib/history.js.map +1 -1
  85. package/lib/store.d.ts +1 -1
  86. package/lib/store.d.ts.map +1 -1
  87. package/lib/store.js +4 -5
  88. package/lib/store.js.map +1 -1
  89. package/lib/utils.d.ts +3 -43
  90. package/lib/utils.d.ts.map +1 -1
  91. package/lib/utils.js +61 -246
  92. package/lib/utils.js.map +1 -1
  93. package/lib/version.d.ts +1 -1
  94. package/lib/version.d.ts.map +1 -1
  95. package/lib/version.js +2 -1
  96. package/lib/version.js.map +1 -1
  97. package/package.json +12 -11
  98. package/src/dist.ts +3 -2
  99. package/src/elements/base/dialog.ts +10 -20
  100. package/src/elements/base/gesture.ts +16 -14
  101. package/src/elements/base/link.ts +65 -44
  102. package/src/elements/base/modal-factory.ts +14 -14
  103. package/src/elements/base/reflect.ts +24 -32
  104. package/src/elements/base/route.ts +55 -68
  105. package/src/elements/base/title.ts +25 -42
  106. package/src/elements/base/unsafe.ts +12 -21
  107. package/src/elements/base/use.ts +17 -13
  108. package/src/helper/i18n.ts +20 -14
  109. package/src/helper/react-utils.ts +66 -0
  110. package/src/helper/request.ts +122 -53
  111. package/src/helper/ssr-shim.ts +8 -1
  112. package/src/helper/theme.ts +72 -45
  113. package/src/index.ts +6 -4
  114. package/src/lib/decorators.ts +394 -93
  115. package/src/lib/directives.ts +6 -0
  116. package/src/lib/element.ts +473 -499
  117. package/src/lib/history.ts +4 -3
  118. package/src/lib/store.ts +2 -1
  119. package/src/lib/utils.ts +19 -205
  120. package/src/lib/version.ts +2 -1
  121. package/src/test/elements/link.test.ts +1 -1
  122. package/src/test/elements/title.test.ts +1 -0
  123. package/src/test/gem-element/advance.test.ts +72 -49
  124. package/src/test/gem-element/basic.test.ts +37 -50
  125. package/src/test/gem-element/decorators.test.ts +22 -21
  126. package/src/test/gem-element/multi.test.ts +5 -5
  127. package/src/test/utils.test.ts +16 -38
@@ -1,37 +1,16 @@
1
- import { html, render, TemplateResult } from 'lit-html';
1
+ import type { TemplateResult } from 'lit-html';
2
+ import { html, render } from 'lit-html';
2
3
 
3
4
  import type { GemReflectElement } from '../elements/reflect';
4
5
 
5
- import { connect, Store } from './store';
6
- import {
7
- LinkedList,
8
- addMicrotask,
9
- Sheet,
10
- SheetToken,
11
- isArrayChange,
12
- GemError,
13
- kebabToCamelCase,
14
- PropProxyMap,
15
- removeItems,
16
- addListener,
17
- } from './utils';
18
- import * as GemExports from './element';
19
- import * as VersionExports from './version';
6
+ import type { Store } from './store';
7
+ import { connect } from './store';
8
+ import { LinkedList, addMicrotask, isArrayChange, addListener, randomStr } from './utils';
20
9
 
21
10
  export { html, svg, render, directive, TemplateResult, SVGTemplateResult } from 'lit-html';
22
- export { repeat } from 'lit-html/directives/repeat';
23
-
24
- // https://github.com/Polymer/lit-html/issues/1048
25
- export { guard } from 'lit-html/directives/guard';
26
-
27
- export { ifDefined } from 'lit-html/directives/if-defined';
28
11
 
29
12
  declare global {
30
- interface ElementInternals extends ARIAMixin {
31
- // https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet
32
- states: Set<string>;
33
- }
34
- // 用于 css 选择器选择元素,使用 @refobject 自动选择获取
13
+ // 用于 css 选择器选择元素,使用 RefObject 自动选择获取
35
14
  // 必须使用 attr 赋值
36
15
  /**
37
16
  * @attr ref
@@ -39,264 +18,447 @@ declare global {
39
18
  interface HTMLElement {
40
19
  ref: string;
41
20
  }
21
+ interface DOMStringMap {
22
+ gemReflect?: '';
23
+ gemStyle?: '';
24
+ [name: string]: string | undefined;
25
+ }
26
+ // https://github.com/tc39/proposal-decorator-metadata
27
+ interface SymbolConstructor {
28
+ readonly metadata: symbol;
29
+ }
42
30
  }
43
31
 
44
- function emptyFunction() {
45
- // 用于占位的空函数
32
+ const { assign, defineProperty, setPrototypeOf } = Object;
33
+
34
+ defineProperty(Symbol, 'metadata', { value: Symbol.for('Symbol.metadata') });
35
+
36
+ const execCallback = (fun: any) => typeof fun === 'function' && fun();
37
+
38
+ // 跨多个 gem 工作
39
+ export const SheetToken = Symbol.for('gem@sheetToken');
40
+ // proto prop
41
+ export const UpdateToken = Symbol.for('gem@update');
42
+
43
+ export const BoundaryCSSState = 'gem-style-boundary';
44
+ export const RenderErrorEvent = 'gem@render-error';
45
+
46
+ // fix modal-factory type error
47
+ const updateTokenAlias = UpdateToken;
48
+
49
+ const rootStyleSheetInfo = new WeakMap<Document | ShadowRoot, Map<CSSStyleSheet, number>>();
50
+ const rootUpdateFnMap = new WeakMap<Map<CSSStyleSheet, number>, () => void>();
51
+
52
+ const getReflectTargets = (ele: ShadowRoot | GemElement) =>
53
+ [...ele.querySelectorAll<GemReflectElement>('[data-gem-reflect]')].map((e) => e.target);
54
+
55
+ class RefObject<T = HTMLElement> {
56
+ refSelector: string;
57
+ ele: GemElement | ShadowRoot;
58
+ ref: string;
59
+
60
+ constructor(current: GemElement) {
61
+ const ref = `ref-${randomStr()}`;
62
+ this.refSelector = `[ref=${ref}]`;
63
+ this.ele = current.internals.shadowRoot || current;
64
+ this.ref = ref;
65
+ }
66
+ get element() {
67
+ for (const e of [this.ele, ...getReflectTargets(this.ele)]) {
68
+ // 在 LightDOM 中可能工作很慢?
69
+ const result = e.querySelector(this.refSelector);
70
+ if (result) return result as T;
71
+ }
72
+ }
73
+ get elements() {
74
+ return [this.ele, ...getReflectTargets(this.ele)]
75
+ .map((e) => [...e.querySelectorAll(this.refSelector)] as T[])
76
+ .flat();
77
+ }
78
+ toString() {
79
+ return this.ref;
80
+ }
46
81
  }
47
82
 
48
- function execCallback(fun: any) {
49
- typeof fun === 'function' && fun();
83
+ /**必须使用在字段中,否则会读取到错误的实例 */
84
+ export let createRef: <T>() => RefObject<T>;
85
+
86
+ class GemCSSStyleSheet extends CSSStyleSheet {
87
+ #ele?: HTMLStyleElement;
88
+
89
+ cssText = '';
90
+ get element() {
91
+ if (!this.#ele) {
92
+ this.#ele = document.createElement('style');
93
+ this.#ele.textContent = this.cssText;
94
+ this.#ele.dataset.gemStyle = '';
95
+ }
96
+ return this.#ele;
97
+ }
98
+ replaceSync(text: string) {
99
+ this.cssText = text;
100
+ if (this.#ele) this.#ele.textContent = text;
101
+ super.replaceSync(text);
102
+ }
50
103
  }
51
104
 
52
- // global render task pool
53
- const asyncRenderTaskList = new LinkedList<() => void>();
54
- const tick = (timeStamp = performance.now()) => {
55
- if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick);
56
- const task = asyncRenderTaskList.get();
57
- if (task) {
58
- task();
59
- tick(timeStamp);
105
+ class GemCSSSheet {
106
+ #content = '';
107
+ #media = '';
108
+ constructor(media = '') {
109
+ this.#media = media;
110
+ }
111
+ setContent(v: string) {
112
+ this.#content = v;
113
+ }
114
+
115
+ // 不需要 GC
116
+ #record = new Map<any, GemCSSStyleSheet>();
117
+ #used = new Map<GemCSSStyleSheet, string>();
118
+ getStyle(host?: HTMLElement) {
119
+ const isLight = host && !(host as GemElement).internals?.shadowRoot;
120
+
121
+ // 对同一类 dom 只使用同一个样式表
122
+ const key = isLight ? host.constructor : this;
123
+ if (!this.#record.has(key)) {
124
+ const sheet = new GemCSSStyleSheet({ media: this.#media });
125
+ this.#record.set(key, sheet);
126
+ }
127
+
128
+ const sheet = this.#record.get(key)!;
129
+
130
+ // 只执行一次
131
+ if (!this.#used.has(sheet)) {
132
+ let style = this.#content;
133
+ let scope = '';
134
+ if (isLight) {
135
+ // light dom 嵌套时需要选择子元素
136
+ // `> *` 实际上是多范围?是否存在性能问题
137
+ scope = `@scope (${host.tagName}) to (:state(${BoundaryCSSState}) > *)`;
138
+ // 不能使用 @layer,两个 @layer 不能覆盖,只有顺序起作用
139
+ // 所以外部不能通过元素名称选择器来覆盖样式,除非样式在之前插入(会自动反转应用到尾部, see `appleCSSStyleSheet`)
140
+ style = `${scope}{ ${style} }`;
141
+ }
142
+ sheet.replaceSync(style);
143
+ this.#used.set(sheet, scope);
144
+ }
145
+
146
+ return sheet;
147
+ }
148
+
149
+ // 一般用于主题更新,不支持 layer
150
+ updateStyle() {
151
+ this.#used.forEach((scope, sheet) => {
152
+ sheet.replaceSync(scope ? `${scope}{${this.#content}}` : this.#content);
153
+ });
154
+ }
155
+ }
156
+
157
+ export type Sheet<T> = { [P in keyof T]: P } & { [SheetToken]: GemCSSSheet };
158
+
159
+ /**
160
+ *
161
+ * 创建 style sheet 用于 `@adoptedStyle`,不支持样式更新,只支持自定义 CSS 属性
162
+ */
163
+ export function createCSSSheet<T extends Record<string, string>>(media: string, rules: T | string): Sheet<T>;
164
+ export function createCSSSheet<T extends Record<string, string>>(rules: T | string): Sheet<T>;
165
+ export function createCSSSheet<T extends Record<string, string>>(
166
+ mediaOrRules: T | string,
167
+ rulesValue?: T | string,
168
+ ): Sheet<T> {
169
+ const media = rulesValue ? (mediaOrRules as string) : '';
170
+ const rules = rulesValue || mediaOrRules;
171
+ const styleSheet = new GemCSSSheet(media);
172
+ const sheet: any = { [SheetToken]: styleSheet };
173
+ let style = '';
174
+ if (typeof rules === 'string') {
175
+ style = rules;
176
+ } else {
177
+ Object.keys(rules).forEach((key) => {
178
+ const isScope = key === '$';
179
+ // 对于已经有 `-` 的保留原始 key,支持覆盖修改
180
+ sheet[key] = isScope || key.includes('-') ? key : `${key}-${randomStr()}`;
181
+ style += `${isScope ? ':scope,:host' : `.${sheet[key]}`} {${rules[key]}}`;
182
+ });
60
183
  }
184
+ styleSheet.setContent(style);
185
+ return sheet as Sheet<T>;
186
+ }
187
+
188
+ const updateStyleSheets = (map: Map<CSSStyleSheet, number>, sheets: CSSStyleSheet[], value: number) => {
189
+ let needUpdate = false;
190
+ sheets.forEach((e) => {
191
+ const count = map.get(e) || 0;
192
+ needUpdate ||= (value === 1 && count === 0) || (value === -1 && count === 1);
193
+ // count 为 0 时不立即删除,以便更新时识别是否外部样式
194
+ // 但是只要挂载 document 的样式表就不会被 GC 了, 是否要在闲时 GC?
195
+ map.set(e, count + value);
196
+ });
197
+ if (needUpdate) {
198
+ // 避免重复更新,例如 list 元素增删,全 light dom 时 document 更新
199
+ addMicrotask(rootUpdateFnMap.get(map)!);
200
+ }
201
+ };
202
+
203
+ const appleCSSStyleSheet = (ele: HTMLElement, sheets: CSSStyleSheet[]) => {
204
+ const root = ele.getRootNode() as ShadowRoot | Document;
205
+ if (!rootStyleSheetInfo.has(root)) {
206
+ const map = new Map<CSSStyleSheet, number>();
207
+ rootStyleSheetInfo.set(root, map);
208
+ rootUpdateFnMap.set(map, () => {
209
+ // 先找到外部样式表
210
+ const newSheets = root.adoptedStyleSheets.filter((e) => !map.has(e));
211
+ map.forEach((count, sheet) => count && newSheets.push(sheet));
212
+ // 外层元素的样式要放到最后,以提升优先级,但是只考虑第一次出现样式表的位置
213
+ root.adoptedStyleSheets = newSheets.reverse();
214
+ });
215
+ }
216
+
217
+ const map = rootStyleSheetInfo.get(root)!;
218
+
219
+ updateStyleSheets(map, sheets, 1);
220
+ return () => updateStyleSheets(map, sheets, -1);
61
221
  };
62
- asyncRenderTaskList.addEventListener('start', () => addMicrotask(tick));
222
+
223
+ /**必须使用在字段中,否则会读取到错误的实例 */
224
+ export let createState: <T>(initState: T) => T & ((state: Partial<T>) => void);
63
225
 
64
226
  type GetDepFun<T> = () => T;
65
227
  type EffectCallback<T> = (depValues: T, oldDepValues?: T) => any;
66
228
  type EffectItem<T> = {
67
229
  callback: EffectCallback<T>;
68
230
  initialized?: boolean;
69
- inConstructor?: boolean;
231
+ fixed?: boolean; // 不需要清理,只会添加一次
70
232
  values?: T;
71
233
  getDep?: GetDepFun<T>;
72
234
  preCallback?: () => void;
73
235
  };
74
236
 
75
- const constructorSymbol = Symbol('constructor');
76
- const initSymbol = Symbol('init');
77
- const updateSymbol = Symbol('update');
237
+ function execEffectList(list?: EffectItem<any>[]) {
238
+ list?.forEach((effectItem) => {
239
+ const { callback, getDep, values, preCallback } = effectItem;
240
+ const newValues = getDep?.();
241
+ if (!getDep || !values || isArrayChange(values, newValues)) {
242
+ execCallback(preCallback);
243
+ effectItem.preCallback = callback(newValues, values);
244
+ effectItem.values = newValues;
245
+ // 更新时也要设置,避免异步的 `#initEffect` 重复执行
246
+ effectItem.initialized = true;
247
+ }
248
+ });
249
+ }
78
250
 
79
- export interface GemElementOptions extends Partial<ShadowRootInit> {
80
- isLight?: boolean;
81
- isAsync?: boolean;
82
- focusable?: boolean;
251
+ function clearEffect(list: EffectItem<any>[]) {
252
+ return list.filter((e) => {
253
+ execCallback(e.preCallback);
254
+ e.initialized = false;
255
+ e.values = undefined;
256
+ return e.fixed;
257
+ });
83
258
  }
84
259
 
85
- export abstract class GemElement<T = Record<string, unknown>> extends HTMLElement {
86
- // 禁止覆盖自定义元素原生生命周期方法
87
- // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226
88
- static #final = Symbol();
260
+ type Render = () => TemplateResult | null | undefined;
261
+ type RenderItem = { render: Render; condition?: () => boolean };
89
262
 
90
- // 这里只是字段申明,不能赋值,否则子类会继承被共享该字段
91
- static observedAttributes?: string[]; // WebAPI 中是实时检查这个列表
92
- static booleanAttributes?: Set<string>;
93
- static numberAttributes?: Set<string>;
94
- static observedProperties?: string[];
95
- static observedStores?: Store<unknown>[];
96
- static adoptedStyleSheets?: Sheet<unknown>[];
97
- static defineEvents?: string[];
98
- static defineCSSStates?: string[];
99
- static defineRefs?: string[];
263
+ export let createTemplate: (ele: GemElement, item: RenderItem) => void;
264
+
265
+ export type Metadata = Partial<ShadowRootInit> & {
266
+ noBlocking?: boolean;
267
+ aria?: Partial<
268
+ ARIAMixin & {
269
+ /**自动添加 tabIndex;支持 `disabled` 属性 */
270
+ focusable: boolean;
271
+ }
272
+ >;
273
+ // 实例化时使用到,DevTools 需要读取
274
+ observedStores: Store<unknown>[];
275
+ adoptedStyleSheets?: Sheet<unknown>[];
100
276
  // 以下静态字段仅供外部读取,没有实际作用
101
- static defineParts?: string[];
102
- static defineSlots?: string[];
103
- // 指定 root 元素类型
104
- static rootElement?: string;
277
+ observedProperties?: string[];
278
+ observedAttributes?: string[];
279
+ definedEvents?: string[];
280
+ definedCSSStates?: string[];
281
+ definedParts?: string[];
282
+ definedSlots?: string[];
283
+ /**@deprecated GemDevtools 在使用*/
284
+ definedRefs?: string[];
285
+ };
105
286
 
106
- // 定义当前元素的状态,和 attr/prop 的本质区别是不为外部输入
107
- readonly state?: T;
287
+ // global render task pool
288
+ const noBlockingTaskList = new LinkedList<() => void>();
289
+ const tick = (timeStamp = performance.now()) => {
290
+ if (performance.now() > timeStamp + 16) return requestAnimationFrame(tick);
291
+ const task = noBlockingTaskList.get();
292
+ if (task) {
293
+ task();
294
+ tick(timeStamp);
295
+ }
296
+ };
297
+ noBlockingTaskList.addEventListener('start', () => addMicrotask(tick));
298
+
299
+ let currentConstructGemElement: GemElement;
300
+
301
+ export abstract class GemElement extends HTMLElement {
302
+ // 禁止覆盖自定义元素原生生命周期方法
303
+ // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226
304
+ static #final = Symbol();
108
305
 
109
306
  #renderRoot: HTMLElement | ShadowRoot;
110
- #internals?: ElementInternals;
307
+ #internals: ElementInternals & {
308
+ // 有别于 adoptedStyleSheets,用来定义单个实例样式
309
+ sheets: GemCSSStyleSheet[];
310
+ stateList: ReturnType<typeof createState>[];
311
+ };
312
+ #effectList: EffectItem<any>[] = [];
313
+ #memoList: EffectItem<any>[] = [];
314
+ #renderList: RenderItem[] = [];
111
315
  #isAppendReason?: boolean;
112
- // 和 isConnected 有区别
113
316
  #isMounted?: boolean;
114
- #isAsync?: boolean;
115
- #effectList?: EffectItem<any>[];
116
- #memoList?: EffectItem<any>[];
117
- #unmountCallback?: any;
317
+ // not in constructor 的近似值
318
+ #isConnected?: boolean;
319
+ #rendering?: boolean;
320
+ #clearStyle?: any;
321
+
322
+ [updateTokenAlias]() {
323
+ // 避免 `connectedCallback` 中的 property 赋值造成多余更新
324
+ if (this.#isMounted) {
325
+ addMicrotask(this.#update);
326
+ }
327
+ }
118
328
 
119
- constructor(options: GemElementOptions = {}) {
329
+ static {
330
+ createState = <T>(initState: T) => {
331
+ const ele = currentConstructGemElement;
332
+ const state: any = (payload: Partial<T>) => {
333
+ assign(state, payload);
334
+ // 避免无限刷新
335
+ if (!ele.#rendering) addMicrotask(ele.#update);
336
+ };
337
+ setPrototypeOf(state, null);
338
+ delete state.name;
339
+ delete state.length;
340
+ ele.#internals.stateList.push(state);
341
+ assign(state, initState);
342
+ return state as T & ((this: GemElement, state: Partial<T>) => void);
343
+ };
344
+ createRef = () => new RefObject(currentConstructGemElement);
345
+ createTemplate = (ele, item) => ele.#renderList.push(item);
346
+ }
347
+
348
+ constructor() {
120
349
  super();
121
350
 
122
- // 外部不可见,但允许类外面使用
123
- addMicrotask(() => Reflect.set(this, initSymbol, false));
124
- Reflect.set(this, constructorSymbol, true);
125
- Reflect.set(this, initSymbol, true);
126
- Reflect.set(this, updateSymbol, () => {
127
- if (this.#isMounted) {
128
- addMicrotask(this.#update);
129
- }
130
- });
351
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
352
+ currentConstructGemElement = this;
131
353
 
132
- this.#isAsync = options.isAsync;
133
- this.#renderRoot = options.isLight
134
- ? this
135
- : this.attachShadow({
136
- mode: options.mode || 'open',
137
- delegatesFocus: options.delegatesFocus,
138
- slotAssignment: options.slotAssignment,
139
- });
354
+ const { mode, serializable, delegatesFocus, slotAssignment, aria } = this.#metadata;
355
+ const { focusable, ...internalsAria } = aria || {};
356
+
357
+ this.#renderRoot = !mode ? this : this.attachShadow({ mode, serializable, delegatesFocus, slotAssignment });
358
+ this.#internals = this.attachInternals() as GemElement['internals'];
359
+ this.#internals.stateList = [];
360
+ this.#internals.sheets = [];
361
+
362
+ assign(this.#internals, internalsAria);
363
+
364
+ // light dom 添加边界,防止内容被外部样式化
365
+ // 特殊情况下需要手动 `delete` 边界,以应该外部样式
366
+ if (!mode) this.#internals.states.add(BoundaryCSSState);
140
367
 
141
368
  // https://stackoverflow.com/questions/43836886/failed-to-construct-customelement-error-when-javascript-file-is-placed-in-head
142
369
  // focusable 元素一般同时具备 disabled 属性
143
370
  // 和原生元素行为保持一致,disabled 时不触发 click 事件
144
371
  let hasInitTabIndex: boolean | undefined;
145
- this.effect(
146
- ([disabled = false]) => {
372
+ this.#effectList.push({
373
+ fixed: true,
374
+ getDep: () => [(this as any).disabled],
375
+ callback: ([disabled = false]) => {
147
376
  if (hasInitTabIndex === undefined) hasInitTabIndex = this.hasAttribute('tabindex');
148
-
149
- this.internals.ariaDisabled = String(disabled);
150
-
151
- if (options.focusable && !hasInitTabIndex) {
152
- this.tabIndex = -Number(disabled);
153
- }
154
-
155
- if ((options.focusable || options.delegatesFocus) && disabled) {
377
+ this.#internals.ariaDisabled = String(disabled);
378
+ if (focusable && !hasInitTabIndex) this.tabIndex = -Number(disabled);
379
+ if ((focusable || delegatesFocus) && disabled) {
156
380
  return addListener(this, 'click', (e: Event) => e.isTrusted && e.stopImmediatePropagation(), {
157
381
  capture: true,
158
382
  });
159
383
  }
160
384
  },
161
- () => [Reflect.get(this, 'disabled')],
162
- );
163
-
164
- const { adoptedStyleSheets } = new.target;
165
- if (adoptedStyleSheets) {
166
- const sheets = adoptedStyleSheets.map((item) => item[SheetToken] || item);
167
- if (this.shadowRoot) {
168
- this.shadowRoot.adoptedStyleSheets = sheets;
169
- } else {
170
- this.effect(
171
- () => {
172
- const root = this.getRootNode() as ShadowRoot | Document;
173
- root.adoptedStyleSheets = [...root.adoptedStyleSheets, ...sheets];
174
- return () => {
175
- root.adoptedStyleSheets = removeItems(root.adoptedStyleSheets, sheets);
176
- };
177
- },
178
- () => [],
179
- );
180
- }
181
- }
385
+ });
386
+ }
387
+
388
+ get #metadata(): Metadata {
389
+ return (this.constructor as any)[Symbol.metadata];
390
+ }
391
+
392
+ get #renderItem() {
393
+ return this.#renderList.find((e) => !e.condition || e.condition());
182
394
  }
183
395
 
184
396
  get internals() {
185
- if (!this.#internals) {
186
- this.#internals = this.attachInternals();
187
- // https://groups.google.com/a/chromium.org/g/blink-dev/c/JvpHoUfhJYE?pli=1
188
- // https://bugs.webkit.org/show_bug.cgi?id=215911
189
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1588763
190
- try {
191
- this.#internals.states.add('foo');
192
- this.#internals.states.delete('foo');
193
- } catch {
194
- Reflect.defineProperty(this.#internals, 'states', {
195
- value: {
196
- has: (v: string) => kebabToCamelCase(v) in this.dataset,
197
- add: (v: string) => (this.dataset[kebabToCamelCase(v)] = ''),
198
- delete: (v: string) => delete this.dataset[kebabToCamelCase(v)],
199
- },
200
- });
201
- }
202
- }
203
397
  return this.#internals;
204
398
  }
205
399
 
206
400
  /**
207
- * @helper
208
- * 设置元素 state,会触发更新
209
- *
210
- * ```js
211
- * class App extends GemElement {
212
- * click() {
213
- * this.setState({});
214
- * }
215
- * }
216
- * ```
401
+ * 每次更新完检查依赖,执行对应的副作用回调
217
402
  * */
218
- setState = (payload: Partial<T>) => {
219
- if (!this.state) throw new GemError('`state` not initialized');
220
- Object.assign(this.state, payload);
221
- addMicrotask(this.#update);
403
+ #execEffect = () => {
404
+ execEffectList(this.#effectList);
222
405
  };
223
406
 
224
- #exec = (list?: EffectItem<any>[]) => {
225
- list?.forEach((effectItem) => {
226
- const { callback, getDep, values, preCallback } = effectItem;
227
- const newValues = getDep?.();
228
- if (!getDep || !values || isArrayChange(values, newValues)) {
229
- execCallback(preCallback);
230
- effectItem.preCallback = callback(newValues, values);
231
- effectItem.values = newValues;
232
- }
233
- });
407
+ #execMemo = () => {
408
+ execEffectList(this.#memoList);
234
409
  };
235
410
 
236
411
  /**
237
- * 每次更新完检查依赖,执行对应的副作用回调
412
+ * - 条件渲染
413
+ * - 不提供 `render` 时显示子内容
414
+ * - 返回 `null` 时渲染空的子内容
415
+ * - 返回 `undefined` 时不会更新现有内容
238
416
  * */
239
- #execEffect = () => {
240
- this.#exec(this.#effectList);
417
+ #render = (item?: RenderItem) => {
418
+ try {
419
+ this.#rendering = true;
420
+ this.#execMemo();
421
+ const isLight = this.#renderRoot === this;
422
+ const temp = item ? item.render() : isLight ? undefined : html`<slot></slot>`;
423
+ this.#rendering = false;
424
+ if (temp === undefined) return;
425
+ render(temp, this.#renderRoot);
426
+ } catch (err) {
427
+ this.dispatchEvent(new CustomEvent(RenderErrorEvent, { bubbles: true, composed: true, detail: err }));
428
+ }
241
429
  };
242
430
 
243
- #execMemo = () => {
244
- this.#exec(this.#memoList);
431
+ #updateCallback = () => {
432
+ const item = this.#renderItem;
433
+ if (
434
+ this.#isMounted &&
435
+ // 当有渲染函数,但是没一个符合条件时跳过
436
+ (item || !this.#renderList.length)
437
+ ) {
438
+ this.#render(item);
439
+ addMicrotask(this.#execEffect);
440
+ }
245
441
  };
246
442
 
247
- /**
248
- * @helper
249
- * 记录副作用回调和值,在 `constructor`/`mounted` 中使用
250
- * 回调到返回值如果是函数将再卸载时执行
251
- * 第一次执行时 `oldDeps` 为空
252
- *
253
- * ```js
254
- * class App extends GemElement {
255
- * mounted() {
256
- * this.effect(callback, () => [this.attrName]);
257
- * }
258
- * }
259
- * ```
260
- * */
261
- effect = <T = any[] | undefined>(callback: EffectCallback<T>, getDep?: T extends any[] ? () => T : undefined) => {
262
- if (!this.#effectList) this.#effectList = [];
263
- const effectItem: EffectItem<T> = {
264
- callback,
265
- getDep,
266
- initialized: this.#isMounted,
267
- inConstructor: (this as any)[constructorSymbol],
268
- };
269
- // 以挂载时立即执行副作用,未挂载时等挂载后执行
270
- if (this.#isMounted) {
271
- effectItem.values = getDep?.() as T;
272
- effectItem.preCallback = callback(effectItem.values);
443
+ #update = () => {
444
+ if (this.#metadata.noBlocking) {
445
+ noBlockingTaskList.add(this.#updateCallback);
446
+ } else {
447
+ this.#updateCallback();
273
448
  }
274
- this.#effectList.push(effectItem);
275
449
  };
276
450
 
277
- /**
278
- * @helper
279
- * 在 `render` 前执行回调,不要在里面使用 `setState`
280
- * 和 `effect` 一样接受依赖数组参数,在 `constructor`/`willMount` 中使用;
281
- * 第一次执行时 `oldDeps` 为空
282
- *
283
- * ```js
284
- * class App extends GemElement {
285
- * willMount() {
286
- * this.memo(() => {
287
- * this.a = exec(this.attrName);
288
- * }), () => [this.attrName]);
289
- * }
290
- * }
291
- * ```
292
- * */
293
- memo = <T = any[] | undefined>(callback: EffectCallback<T>, getDep?: T extends any[] ? () => T : undefined) => {
294
- if (!this.#memoList) this.#memoList = [];
295
- this.#memoList.push({
296
- callback,
297
- getDep,
298
- inConstructor: (this as any)[constructorSymbol],
299
- });
451
+ #prepareStyle = () => {
452
+ const { adoptedStyleSheets = [] } = this.#metadata;
453
+ const { shadowRoot, sheets } = this.#internals;
454
+
455
+ const clsSheets = adoptedStyleSheets.map((item) => item[SheetToken].getStyle(this));
456
+ if (shadowRoot) {
457
+ shadowRoot.adoptedStyleSheets = clsSheets.concat(sheets);
458
+ } else {
459
+ this.prepend(...sheets.map((e) => e.element));
460
+ return appleCSSStyleSheet(this, clsSheets);
461
+ }
300
462
  };
301
463
 
302
464
  /**
@@ -313,138 +475,54 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
313
475
  });
314
476
  };
315
477
 
316
- /**@lifecycle */
317
- willMount?(): void | Promise<void>;
318
-
319
- /**
320
- * @lifecycle
321
- *
322
- * - 不提供 `render` 时显示子内容
323
- * - 返回 `null` 时渲染空内容
324
- * - 返回 `undefined` 时不会调用 `render()`, 也就是不会更新以前的内容
325
- * */
326
- render?(): TemplateResult | null | undefined;
327
-
328
- #render = () => {
329
- this.#execMemo();
330
- const isLight = this.#renderRoot === this;
331
- const temp = this.render ? this.render() : isLight ? undefined : html`<slot></slot>`;
332
- if (temp === undefined) return;
333
- render(temp, this.#renderRoot);
334
- };
335
-
336
- /**@lifecycle */
337
- mounted?(): void | (() => void) | Promise<void>;
338
-
339
- /**@lifecycle */
340
- shouldUpdate?(): boolean;
341
- #shouldUpdate = () => {
342
- return this.shouldUpdate ? this.shouldUpdate() : true;
343
- };
344
-
345
- #updateCallback = () => {
346
- if (this.#isMounted && this.#shouldUpdate()) {
347
- this.#render();
348
- addMicrotask(this.#updated);
349
- addMicrotask(this.#execEffect);
350
- }
351
- };
352
-
353
- #update = () => {
354
- if (this.#isAsync) {
355
- asyncRenderTaskList.add(this.#updateCallback);
356
- } else {
357
- this.#updateCallback();
358
- }
359
- };
360
-
361
- /**
362
- * @helper
363
- * async
364
- */
365
- update = () => {
366
- addMicrotask(this.#update);
478
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ willMount?(): void | Promise<void>;
479
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ mounted?(): void | (() => void) | Promise<void>;
480
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ updated?(): void | Promise<void>;
481
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ unmounted?(): void | Promise<void>;
482
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ shouldUpdate?(): boolean;
483
+ /** @lifecycle 将来会彻底移除,现在做兼容实现*/ render?(): TemplateResult | null | undefined;
484
+ #compat = () => {
485
+ if (this.willMount) this.memo(this.willMount.bind(this), () => []);
486
+ if (this.mounted) this.effect(this.mounted.bind(this), () => []);
487
+ if (this.updated) this.effect((_, old) => old && this.updated!.apply(this));
488
+ if (this.unmounted)
489
+ this.effect(
490
+ () => this.unmounted!.bind(this),
491
+ () => [],
492
+ );
493
+ if (!this.#renderList.length && this.render)
494
+ this.#renderList.push({ render: this.render.bind(this), condition: this.shouldUpdate?.bind(this) });
367
495
  };
368
496
 
369
- /**@lifecycle */
370
- updated?(): void | Promise<void>;
371
- #updated = () => {
372
- this.updated?.();
373
- };
374
-
375
- /**
376
- * @lifecycle
377
- *
378
- * 卸载元素,但不一定会被垃圾回收,所以事件监听器需要手动清除,以免重复注册
379
- */
380
- unmounted?(): void | Promise<void>;
381
-
382
- /**
383
- * @private
384
- * @final
385
- * use `effect`
386
- */
387
- attributeChangedCallback() {
388
- if (this.#isMounted) {
389
- addMicrotask(this.#update);
390
- }
391
- return GemElement.#final;
392
- }
393
-
394
497
  #disconnectStore?: (() => void)[];
395
- #connectedCallback = () => {
498
+ #connectedCallback = async () => {
396
499
  if (this.#isAppendReason) {
397
500
  this.#isAppendReason = false;
398
501
  return;
399
502
  }
503
+ this.#compat();
400
504
 
401
- // 似乎这是最早的判断不在 `constructor` 中的地方
402
- Reflect.set(this, constructorSymbol, false);
505
+ const { observedStores } = this.#metadata;
403
506
 
404
- this.willMount?.();
405
- const { observedStores, rootElement } = this.constructor as typeof GemElement;
507
+ this.#isConnected = true;
406
508
  this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
407
- this.#render();
509
+ this.#render(this.#renderItem);
408
510
  this.#isMounted = true;
409
- this.#unmountCallback = this.mounted?.();
511
+ this.#clearStyle = this.#prepareStyle();
512
+ // 等待所有元素的样式被应用,再执行回调
513
+ // 这让 mounted 和 effect 回调和其他回调一样保持一样的异步行为
514
+ await Promise.resolve();
410
515
  this.#initEffect();
411
- if (
412
- rootElement &&
413
- this.isConnected &&
414
- (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase()
415
- ) {
416
- throw new GemError(`not allow current root type`);
417
- }
418
516
  };
419
517
 
420
- closestElement<K extends keyof HTMLElementTagNameMap>(tag: K): HTMLElementTagNameMap[K] | null;
421
- closestElement<K extends abstract new (...args: any) => any>(constructor: K): InstanceType<K> | null;
422
- closestElement<K extends abstract new (...args: any) => any>(constructorOrTag: K | string): Element | null {
423
- const isConstructor = typeof constructorOrTag === 'function';
424
- const tagName = typeof constructorOrTag === 'string' && constructorOrTag.toUpperCase();
425
- const getRootElement = (ele: Element): Element | null => {
426
- const rootEle = ele.parentElement || (ele.getRootNode() as ShadowRoot).host;
427
- if (!rootEle) return null;
428
- if (isConstructor) {
429
- if (rootEle.constructor === constructorOrTag) {
430
- return rootEle;
431
- }
432
- } else if (rootEle.tagName === tagName) {
433
- return rootEle;
434
- }
435
- return getRootElement(rootEle);
436
- };
437
- return getRootElement(this);
438
- }
439
-
440
518
  /**
441
519
  * @private
442
520
  * @final
443
- * use `mounted`; 允许手动调用 `connectedCallback` 以清除装饰器定义的字段
521
+ * use `mounted`
444
522
  */
445
523
  connectedCallback() {
446
- if (this.isConnected && this.#isAsync) {
447
- asyncRenderTaskList.add(this.#connectedCallback);
524
+ if (this.#metadata.noBlocking) {
525
+ noBlockingTaskList.add(this.#connectedCallback);
448
526
  } else {
449
527
  this.#connectedCallback();
450
528
  }
@@ -469,178 +547,74 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
469
547
  this.#isAppendReason = true;
470
548
  return;
471
549
  }
550
+ noBlockingTaskList.delete(this.#connectedCallback);
551
+ noBlockingTaskList.delete(this.#updateCallback);
472
552
  this.#isMounted = false;
473
553
  this.#disconnectStore?.forEach((disconnect) => disconnect());
474
- execCallback(this.#unmountCallback);
475
- this.unmounted?.();
476
- this.#effectList = this.#clearEffect(this.#effectList);
477
- this.#memoList = this.#clearEffect(this.#memoList);
554
+ // 是否要异步执行回调?
555
+ this.#effectList = clearEffect(this.#effectList);
556
+ this.#memoList = clearEffect(this.#memoList);
557
+ execCallback(this.#clearStyle);
478
558
  return GemElement.#final;
479
559
  }
480
560
 
481
- #clearEffect = (list?: EffectItem<any>[]) => {
482
- return list?.filter((e) => {
483
- execCallback(e.preCallback);
484
- e.initialized = false;
485
- return e.inConstructor;
486
- });
561
+ /**
562
+ * @helper
563
+ * 记录副作用回调和值;
564
+ * 回调到返回值如果是函数将再卸载时执行;
565
+ * 第一次执行时 `oldDeps` 为空;
566
+ *
567
+ * ```js
568
+ * class App extends GemElement {
569
+ * mounted() {
570
+ * this.effect(callback, () => [this.attrName]);
571
+ * }
572
+ * }
573
+ * ```
574
+ * */
575
+ effect = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
576
+ const effectItem: EffectItem<K> = {
577
+ callback,
578
+ getDep,
579
+ initialized: this.#isMounted,
580
+ fixed: !this.#isConnected,
581
+ };
582
+ // 已挂载时立即执行副作用,未挂载时等挂载后执行
583
+ if (this.#isMounted) {
584
+ effectItem.values = getDep?.() as K;
585
+ effectItem.preCallback = callback(effectItem.values);
586
+ }
587
+ this.#effectList.push(effectItem);
487
588
  };
488
- }
489
-
490
- const gemElementProxyMap = new PropProxyMap<GemElement>();
491
-
492
- export function defineAttribute(target: GemElement, prop: string, attr: string) {
493
- const { booleanAttributes, numberAttributes } = target.constructor as typeof GemElement;
494
- Object.defineProperty(target, prop, {
495
- configurable: true,
496
- get() {
497
- // fix karma test
498
- // 判断是否是自定义元素实例
499
- if (!(initSymbol in this)) return;
500
- const that = this as GemElement;
501
- // 不能从 proxy 对象中读取值
502
- const value = that.getAttribute(attr);
503
- if (booleanAttributes?.has(attr)) {
504
- return value === null ? false : true;
505
- }
506
- if (numberAttributes?.has(attr)) {
507
- return Number(value);
508
- }
509
- return value || '';
510
- },
511
- set(v: string | null | undefined | number | boolean) {
512
- const that = this as GemElement;
513
- const proxy = gemElementProxyMap.get(this);
514
- const hasSet = proxy[prop];
515
- const value = that.getAttribute(attr);
516
- // https://github.com/whatwg/dom/issues/922
517
- if (this[initSymbol] && value !== null && !hasSet) return;
518
- // 字段和构造函数中都有对 attr 设置时会执行多次
519
- // Firefox WebConsole 中不知道为什么在构造函数中 this[initSymbol] 已经为 false
520
- proxy[prop] = true;
521
- const isBool = booleanAttributes?.has(attr);
522
- if (v === null || v === undefined) {
523
- that.removeAttribute(attr);
524
- } else if (isBool) {
525
- // 当 attr 存在且 !!v 为 true 时,toggleAttribute 不会造成 Attribute 改变
526
- that.toggleAttribute(attr, !!v);
527
- } else {
528
- if (value !== String(v)) that.setAttribute(attr, String(v));
529
- }
530
- },
531
- });
532
- }
533
589
 
534
- const isEventHandleSymbol = Symbol('event handle');
535
- export function defineProperty(
536
- target: GemElement,
537
- prop: string,
538
- event?: string,
539
- eventOptions?: Omit<CustomEventInit<unknown>, 'detail'>,
540
- ) {
541
- Object.defineProperty(target, prop, {
542
- configurable: true,
543
- get() {
544
- const value = gemElementProxyMap.get(this)[prop];
545
- if (value || !event) {
546
- return value;
547
- } else {
548
- this[prop] = emptyFunction;
549
- return this[prop];
550
- }
551
- },
552
- set(v) {
553
- const that = this as GemElement;
554
- const proxy = gemElementProxyMap.get(that);
555
- if (v !== proxy[prop]) {
556
- if (event) {
557
- proxy[prop] = v?.[isEventHandleSymbol]
558
- ? v
559
- : (detail: any, options: any) => {
560
- const evt = new CustomEvent(event, { ...options, ...eventOptions, detail });
561
- that.dispatchEvent(evt);
562
- v(detail, options);
563
- };
564
- Reflect.set(proxy[prop]!, isEventHandleSymbol, true);
565
- // emitter 不触发元素更新
566
- } else {
567
- proxy[prop] = v;
568
- this[updateSymbol]();
569
- }
570
- }
571
- },
572
- });
573
- }
574
-
575
- export function defineRef(target: GemElement, prop: string, ref: string) {
576
- Object.defineProperty(target, prop, {
577
- configurable: true,
578
- get() {
579
- const proxy = gemElementProxyMap.get(this);
580
- let refobject = proxy[prop];
581
- if (!refobject) {
582
- const that = this as GemElement;
583
- const ele = that.shadowRoot || that;
584
- refobject = {
585
- get ref() {
586
- return ref;
587
- },
588
- get element() {
589
- const gemReflects = [...ele.querySelectorAll<GemReflectElement>('[data-gem-reflect]')].map((e) => e.target);
590
- for (const e of [ele, ...gemReflects]) {
591
- const result = e.querySelector(`[ref=${ref}]`);
592
- if (result) return result;
593
- }
594
- },
595
- };
596
- proxy[prop] = refobject;
597
- }
598
- return refobject;
599
- },
600
- });
601
- }
602
-
603
- export function defineCSSState(target: GemElement, prop: string, state: string) {
604
- Object.defineProperty(target, prop, {
605
- configurable: true,
606
- get() {
607
- const that = this as GemElement;
608
- const { states } = that.internals;
609
- return states?.has(state);
610
- },
611
- set(v: boolean) {
612
- const that = this as GemElement;
613
- const { states } = that.internals;
614
- if (v) {
615
- states?.add(state);
616
- } else {
617
- states?.delete(state);
618
- }
619
- },
620
- });
621
- }
622
-
623
- export const nativeDefineElement = customElements.define.bind(customElements);
624
- customElements.define = (name: string, cls: CustomElementConstructor, options?: ElementDefinitionOptions) => {
625
- if (cls.prototype instanceof GemElement) {
626
- const { observedAttributes, observedProperties, defineEvents, defineCSSStates, defineRefs } =
627
- cls as unknown as typeof GemElement;
628
- observedAttributes?.forEach((attr) => defineAttribute(cls.prototype, kebabToCamelCase(attr), attr));
629
- observedProperties?.forEach((prop) => defineProperty(cls.prototype, prop));
630
- defineEvents?.forEach((event) => defineProperty(cls.prototype, kebabToCamelCase(event), event));
631
- defineCSSStates?.forEach((state) => defineCSSState(cls.prototype, kebabToCamelCase(state), state));
632
- defineRefs?.forEach((ref) => defineRef(cls.prototype, kebabToCamelCase(ref), ref));
633
- }
634
-
635
- nativeDefineElement(name, cls, options);
636
- };
637
-
638
- declare global {
639
- interface Window {
640
- __GEM_DEVTOOLS__HOOK__?: (typeof GemExports & typeof VersionExports) | Record<string, never>;
641
- }
642
- }
590
+ /**
591
+ * @helper
592
+ * 在 `render` 前执行回调;
593
+ * 和 `effect` 一样接受依赖数组参数,在 `constructor` 中使用;
594
+ * 第一次执行时 `oldDeps` 为空;
595
+ *
596
+ * ```js
597
+ * class App extends GemElement {
598
+ * constructor() {
599
+ * super();
600
+ * this.memo(() => {
601
+ * this.a = exec(this.attrName);
602
+ * }), () => [this.attrName]);
603
+ * }
604
+ * }
605
+ * ```
606
+ * */
607
+ memo = <K = any[] | undefined>(callback: EffectCallback<K>, getDep?: K extends any[] ? () => K : undefined) => {
608
+ this.#memoList.push({
609
+ callback,
610
+ getDep,
611
+ fixed: !this.#isConnected,
612
+ });
613
+ };
643
614
 
644
- if (window.__GEM_DEVTOOLS__HOOK__) {
645
- Object.assign(window.__GEM_DEVTOOLS__HOOK__, { ...GemExports, ...VersionExports });
615
+ /**
616
+ * @helper
617
+ * async
618
+ */
619
+ update = () => addMicrotask(this.#update);
646
620
  }