Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion etc/runtime.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type { BatchUpdateContextItem } from '@next-core/types';
import type { BootstrapData } from '@next-core/types';
import { BreadcrumbItemConf } from '@next-core/types';
import type { BrickConf } from '@next-core/types';
import type { BrickEventHandler } from '@next-core/types';
import type { BrickEventHandlerCallback } from '@next-core/types';
import type { BrickEventsMap } from '@next-core/types';
import type { BrickLifeCycle } from '@next-core/types';
Expand Down Expand Up @@ -333,6 +332,10 @@ function mountUseBrick({ renderRoot, rendererContext, scopedStores }: RenderUseB

// @public (undocumented)
interface MountUseBrickResult {
// Warning: (ae-forgotten-export) The symbol "Dispose" needs to be exported by the entry point index.d.ts
//
// (undocumented)
dispose: Dispose;
Comment on lines +327 to +330
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Script executed:

#!/bin/bash
# Find the entry point file for the runtime package
find . -path "*/packages/runtime/*" -type f \( -name "index.ts" -o -name "index.d.ts" \) | head -10

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

./packages/runtime/src/index.ts

</shell_result>


Script executed:

#!/bin/bash
# Read the entry point file to check current exports
cat -n ./packages/runtime/src/index.ts

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

     1	export * from "./auth.js";
     2	export * from "./CustomEditors.js";
     3	export * from "./CustomProcessors.js";
     4	export { customTemplates } from "./CustomTemplates.js";
     5	export * from "./fetchByProvider.js";
     6	export * from "./getBasePath.js";
     7	export * from "./getPageInfo.js";
     8	export * from "./handleHttpError.js";
     9	export * from "./history.js";
    10	export * from "./createRoot.js";
    11	export * from "./getRealValue.js";
    12	export {
    13	  createRuntime,
    14	  getRuntime,
    15	  type RuntimeOptions,
    16	  type RuntimeHooks,
    17	  type RuntimeHooksMenuHelpers,
    18	  type ImagesFactory,
    19	  type PageViewInfo,
    20	} from "./internal/Runtime.js";
    21	import * as __secret_internals from "./internal/secret_internals.js";
    22	export { __secret_internals };
    23	export { __test_only } from "./internal/test_only.js";
    24	export {
    25	  getCssPropertyValue,
    26	  getCurrentTheme,
    27	  getCurrentMode,
    28	  getThemeVariant,
    29	  batchSetAppsLocalTheme,
    30	  applyTheme,
    31	} from "./themeAndMode.js";
    32	export {
    33	  checkIfOfComputed,
    34	  checkIfByTransform,
    35	} from "./internal/compute/checkIf.js";
    36	export { registerWidgetFunctions } from "./internal/compute/WidgetFunctions.js";
    37	export { registerWidgetI18n } from "./internal/compute/WidgetI18n.js";
    38	export { StoryboardFunctionRegistryFactory } from "./StoryboardFunctionRegistry.js";
    39	export { matchPath, type MatchOptions } from "./internal/matchPath.js";
    40	export { Notification, type NotificationOptions } from "./Notification.js";
    41	export { Dialog, type DialogOptions } from "./Dialog.js";
    42	export * from "./getV2RuntimeFromDll.js";
    43	export { setUIVersion } from "./setUIVersion.js";
    44	export * from "./ModalStack.js";
    45	export * from "./isNetworkError.js";
    46	export * from "./shouldReloadForError.js";

</shell_result>


Script executed:

#!/bin/bash
# Search for MountUseBrickResult to see where it's defined and exported
rg -n "MountUseBrickResult" --type ts -A 2 -B 2 | head -40

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

packages/runtime/src/internal/secret_internals.ts-132-}
packages/runtime/src/internal/secret_internals.ts-133-
packages/runtime/src/internal/secret_internals.ts:134:export interface MountUseBrickResult {
packages/runtime/src/internal/secret_internals.ts-135-  portal?: HTMLElement;
packages/runtime/src/internal/secret_internals.ts-136-  dispose: Dispose;
--
packages/runtime/src/internal/secret_internals.ts-140-  { renderRoot, rendererContext, scopedStores }: RenderUseBrickResult,
packages/runtime/src/internal/secret_internals.ts-141-  element: HTMLElement
packages/runtime/src/internal/secret_internals.ts:142:): MountUseBrickResult {
packages/runtime/src/internal/secret_internals.ts-143-  let portal: HTMLElement | undefined;
packages/runtime/src/internal/secret_internals.ts-144-  renderRoot.createPortal = () => {
--
packages/runtime/src/internal/secret_internals.ts-170-export function unmountUseBrick(
packages/runtime/src/internal/secret_internals.ts-171-  { rendererContext }: RenderUseBrickResult,
packages/runtime/src/internal/secret_internals.ts:172:  mountResult: MountUseBrickResult
packages/runtime/src/internal/secret_internals.ts-173-): void {
packages/runtime/src/internal/secret_internals.ts-174-  // if (mountResult.mainBrick) {
--
packages/runtime/src/internal/Renderer.spec.ts-108-  "my-use-brick",
packages/runtime/src/internal/Renderer.spec.ts-109-  class MyUseBrick extends HTMLElement {
packages/runtime/src/internal/Renderer.spec.ts:110:    #mountResult: __secret_internals.MountUseBrickResult | undefined;
packages/runtime/src/internal/Renderer.spec.ts-111-    #renderResult: __secret_internals.RenderUseBrickResult | undefined;
packages/runtime/src/internal/Renderer.spec.ts-112-    async connectedCallback() {
--
packages/react-runtime/src/ReactUseBrick.tsx-35-  const [renderResult, setRenderResult] =
packages/react-runtime/src/ReactUseBrick.tsx-36-    useState<__secret_internals.RenderUseBrickResult | null>(null);
packages/react-runtime/src/ReactUseBrick.tsx:37:  const mountResult = useRef<__secret_internals.MountUseBrickResult>();
packages/react-runtime/src/ReactUseBrick.tsx-38-  const [renderKey, setRenderKey] = useState<number>();
packages/react-runtime/src/ReactUseBrick.tsx-39-  const IdCounterRef = useRef(0);

</shell_result>


Script executed:

#!/bin/bash
# Check the import of Dispose in secret_internals.ts
head -50 ./packages/runtime/src/internal/secret_internals.ts | grep -n "Dispose\|import"

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

1:import type {
12:import { pick } from "lodash";
13:import {
19:import {
24:import { RendererContext } from "./RendererContext.js";
25:import type { DataStore } from "./data/DataStore.js";
26:import { resolveData } from "./data/resolveData.js";
27:import { asyncComputeRealValue } from "./compute/computeRealValue.js";
28:import type {
39:  Dispose,
41:import { mountTree, unmountTree } from "./mount.js";
42:import { RenderTag } from "./enums.js";
43:import { isStrictMode } from "../isStrictMode.js";
44:import { customTemplates } from "../CustomTemplates.js";
45:import { registerAppI18n } from "./registerAppI18n.js";
46:import { getTplStateStore } from "./CustomTemplates/utils.js";
47:import {

</shell_result>


Script executed:

#!/bin/bash
# Verify the full import statement for Dispose
sed -n '1,45p' ./packages/runtime/src/internal/secret_internals.ts | grep -A 15 "^import type {"

Repository: easyops-cn/next-core


<shell_result>
Repository: easyops-cn/next-core
Exit code: 0

stdout:

import type {
  BrickConf,
  CustomTemplate,
  RouteConf,
  Storyboard,
  UseSingleBrickConf,
  RuntimeSnippet,
  ContextConf,
  BrickPackage,
  RouteConfOfBricks,
} from "@next-core/types";
import { pick } from "lodash";
import {
  _internalApiGetRenderId,
  _internalApiGetRuntimeContext,
  _internalApiGetStoryboardInBootstrapData,
--
import type { DataStore } from "./data/DataStore.js";
import { resolveData } from "./data/resolveData.js";
import { asyncComputeRealValue } from "./compute/computeRealValue.js";
import type {
  DataValueOption,
  PreviewOption,
  PreviewStoryboardPatch,
  RenderRoot,
  RuntimeContext,
  DebugDataValue,
  RuntimeDataVale,
  RuntimeDataValueOption,
  RenderBrick,
  RenderChildNode,
  Dispose,
} from "./interfaces.js";
import { mountTree, unmountTree } from "./mount.js";
import { RenderTag } from "./enums.js";
import { isStrictMode } from "../isStrictMode.js";

</shell_result>


Add Dispose type to entry point exports to resolve API Extractor warning

The warning is valid: Dispose is used in the public API (MountUseBrickResult interface exported via __secret_internals) but not exported from the main entry point. The type is imported from ./internal/interfaces.ts into secret_internals.ts but never re-exported from packages/runtime/src/index.ts. This prevents users from directly referencing the Dispose type despite encountering it in the public API.

To fix this, add Dispose to the exports in packages/runtime/src/index.ts:

export {
  createRuntime,
  getRuntime,
  type RuntimeOptions,
  type RuntimeHooks,
  type RuntimeHooksMenuHelpers,
  type ImagesFactory,
  type PageViewInfo,
  type Dispose,  // Add this line
} from "./internal/Runtime.js";

Or add a separate export from interfaces.ts if Dispose is not already exported from Runtime.js.

🤖 Prompt for AI Agents
In etc/runtime.api.md around lines 335 to 338, the API Extractor warning shows
the Dispose type is referenced in the public API but not exported from the
package entry point; fix by exporting Dispose from the runtime entry
(packages/runtime/src/index.ts) — add Dispose to the export list (or if Dispose
is only declared in interfaces.ts, re-export it from Runtime.js or directly from
interfaces.ts) so the public surface exposes the Dispose type alongside the
other exported runtime types.

// (undocumented)
portal?: HTMLElement;
}
Expand Down
11 changes: 9 additions & 2 deletions packages/runtime/src/createRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
} from "./internal/Renderer.js";
import { RendererContext } from "./internal/RendererContext.js";
import { DataStore } from "./internal/data/DataStore.js";
import type { RenderRoot, RuntimeContext } from "./internal/interfaces.js";
import type {
Dispose,
RenderRoot,
RuntimeContext,
} from "./internal/interfaces.js";
import { mountTree, unmountTree } from "./internal/mount.js";
import { applyMode, applyTheme, setMode, setTheme } from "./themeAndMode.js";
import { RenderTag } from "./internal/enums.js";
Expand Down Expand Up @@ -107,6 +111,7 @@ export function unstable_createRoot(
let unmounted = false;
let rendererContext: RendererContext | undefined;
let clearI18nBundles: Function | undefined;
let disposeMount: Dispose | undefined;
const isolatedRoot = scope === "page" ? undefined : Symbol("IsolatedRoot");

return {
Expand Down Expand Up @@ -253,7 +258,7 @@ export function unstable_createRoot(
applyMode();
}

mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

if (scope === "page") {
window.scrollTo(0, 0);
Expand Down Expand Up @@ -283,6 +288,8 @@ export function unstable_createRoot(
isolatedFunctionRegistry.delete(isolatedRoot);
isolatedTemplateRegistryMap.delete(isolatedRoot);
}
disposeMount?.();
disposeMount = undefined;
unmountTree(container);
if (portal) {
unmountTree(portal);
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime/src/internal/Router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from "./Runtime.js";
import { getPageInfo } from "../getPageInfo.js";
import type {
Dispose,
MenuRequestNode,
RenderRoot,
RuntimeContext,
Expand Down Expand Up @@ -376,7 +377,11 @@ export class Router {
createPortal: portal,
};

let disposeMount: Dispose | undefined;

const cleanUpPreviousRender = (): void => {
disposeMount?.();
disposeMount = undefined;
unmountTree(main);
unmountTree(portal);

Expand Down Expand Up @@ -547,7 +552,7 @@ export class Router {
applyMode();

setUIVersion(currentApp?.uiVersion);
mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

// Scroll to top after each rendering.
// See https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/docs/guides/scroll-restoration.md
Expand Down Expand Up @@ -606,7 +611,7 @@ export class Router {
);
renderRoot.child = node;

mountTree(renderRoot);
disposeMount = mountTree(renderRoot);

// Scroll to top after each rendering.
window.scrollTo(0, 0);
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/internal/bindListeners.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ describe("listenerFactory for handleHttpError", () => {

describe("listenerFactory for event.*", () => {
test("event.preventDefault", () => {
consoleError.mockReturnValue();
const event = { preventDefault: jest.fn() } as any;
listenerFactory(
{
Expand All @@ -684,9 +685,11 @@ describe("listenerFactory for event.*", () => {
runtimeContext
)(event);
expect(event.preventDefault).toHaveBeenCalledWith();
consoleError.mockReset();
});

test("event.stopPropagation", () => {
consoleError.mockReturnValue();
const event = { stopPropagation: jest.fn() } as any;
listenerFactory(
{
Expand All @@ -695,6 +698,7 @@ describe("listenerFactory for event.*", () => {
runtimeContext
)(event);
expect(event.stopPropagation).toHaveBeenCalledWith();
consoleError.mockReset();
});

test("non-Event object", () => {
Expand Down
35 changes: 14 additions & 21 deletions packages/runtime/src/internal/bindListeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { isPreEvaluated } from "./compute/evaluate.js";
import { setProperties } from "./compute/setProperties.js";
import { applyMode, applyTheme } from "../themeAndMode.js";
import type {
Dispose,
ElementHolder,
RuntimeBrickElement,
RuntimeContext,
Expand All @@ -45,39 +46,31 @@ export function bindListeners(
brick: RuntimeBrickElement,
eventsMap: BrickEventsMap | undefined,
runtimeContext: RuntimeContext
): void {
): Dispose {
if (!eventsMap) {
return;
return () => {};
}

const disposables: Dispose[] = [];

Object.entries(eventsMap).forEach(([eventType, handlers]) => {
const listener = listenerFactory(handlers, runtimeContext, {
element: brick,
});
brick.addEventListener(eventType, listener);

// Remember added listeners for unbinding.
if (!brick.$$listeners) {
brick.$$listeners = [];
}
brick.$$listeners.push([eventType, listener]);

// Remember added listeners for devtools.
if (!brick.$$eventListeners) {
brick.$$eventListeners = [];
}
for (const handler of ([] as BrickEventHandler[]).concat(handlers)) {
brick.$$eventListeners.push([eventType, null, handler]);
}
disposables.push(() => {
brick.removeEventListener(eventType, listener);
});
});
}

export function unbindListeners(brick: RuntimeBrickElement): void {
if (brick.$$listeners) {
for (const [eventType, listener] of brick.$$listeners) {
brick.removeEventListener(eventType, listener);
return () => {
for (const dispose of disposables) {
dispose();
}
brick.$$listeners.length = 0;
}
disposables.length = 0;
};
}

export function isBuiltinHandler(
Expand Down
28 changes: 24 additions & 4 deletions packages/runtime/src/internal/data/DataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ResolveOptions, resolveData } from "./resolveData.js";
import { resolveDataStore } from "./resolveDataStore.js";
import type {
AsyncPropertyEntry,
Dispose,
RouteNode,
Comment on lines +22 to 23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

disposableMap 中重复存储同一 Dispose,可能导致长期悬挂引用

目前同一个 Dispose 会被存储两次:

  1. onChange(dataName, listener) 内部调用 this.addDisposable(dataName, disposable);(key 为依赖名 dataName)。
  2. dataConf.track 分支中再次调用 this.addDisposable(dataConf.name, disposable);(key 为当前 data 名)。

在路由销毁逻辑中,disposeDataInRoutes 只按将要删除的 data 名(来自 routeMap)去查询 disposableMap.get(name) 并执行其中的 disposables,然后删除该 key。这样会产生两个问题:

  • 对于作为“依赖”的 data(例如全局 CTX),其 key 下的 Dispose[] 永远不会被 disposeDataInRoutes 删除,即使这些 Dispose 已经在以 dataConf.name 为 key 的数组中被执行过,仍然会在 disposableMap 中保留,持有闭包引用,长期运行有内存上涨风险。
  • 同一个 disposable 被存入两个数组,遇到同时销毁依赖和被依赖 data 的场景时会被调用两次(虽然 DOM/EventTarget 层面是幂等的,但这是不必要的重复工作,也让状态变得更难推理)。

建议把“订阅登记”和“所有者”解耦,避免重复存储同一个 Dispose,比如采取更简单的策略:

  • onChange 仅负责注册监听并返回 Dispose,不在内部调用 addDisposable

    onChange(dataName: string, listener: EventListener): Dispose {
      const eventTarget = this.data.get(dataName)?.eventTarget;
      eventTarget?.addEventListener(this.changeEventType, listener);
  • const disposable = () => {

  • const disposable = () => {
    eventTarget?.removeEventListener(this.changeEventType, listener);
    };
  • this.addDisposable(dataName, disposable);
    return disposable;
    }

- 由真正需要 route 级自动清理的调用方(如 `dataConf.track` 那段)根据“拥有者”名字显式调用 `addDisposable(dataConf.name, disposable)`,避免再按依赖名重复登记。

如果确实有其它地方依赖“按依赖名自动登记”的行为,也可以考虑为 `onChange` 增加一个可选 ownerName 参数,仅以 ownerName 作为 `disposableMap` 的 key 来管理清理。





Also applies to: 75-76, 365-373, 545-563, 586-593, 603-612

<!-- fingerprinting:phantom:poseidon:olive -->

<!-- This is an auto-generated comment by CodeRabbit -->

RuntimeBrick,
RuntimeContext,
Expand Down Expand Up @@ -71,6 +72,7 @@ export class DataStore<T extends DataStoreType = "CTX"> {
private readonly rendererContext?: RendererContext;
private routeMap = new WeakMap<RouteConf, Set<string>>();
private routeStackMap = new WeakMap<RouteConf, Set<PendingStackItem>>();
private disposableMap = new Map<string, Dispose[]>();

// 把 `rendererContext` 放在参数列表的最后,并作为可选,以减少测试文件的调整
constructor(
Expand Down Expand Up @@ -360,12 +362,14 @@ export class DataStore<T extends DataStoreType = "CTX"> {
}
}

onChange(dataName: string, listener: EventListener): () => void {
onChange(dataName: string, listener: EventListener): Dispose {
const eventTarget = this.data.get(dataName)?.eventTarget;
eventTarget?.addEventListener(this.changeEventType, listener);
return () => {
const disposable = () => {
eventTarget?.removeEventListener(this.changeEventType, listener);
};
this.addDisposable(dataName, disposable);
return disposable;
}

async waitFor(dataNames: string[] | Set<string>): Promise<void> {
Expand Down Expand Up @@ -509,7 +513,6 @@ export class DataStore<T extends DataStoreType = "CTX"> {
};

if (resolvePolicy === "lazy") {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { trigger } = dataConf.resolve!;
if (
trigger &&
Expand Down Expand Up @@ -539,7 +542,7 @@ export class DataStore<T extends DataStoreType = "CTX"> {
);
!load && (newData.deps = [...deps]);
for (const dep of deps) {
this.onChange(
const disposable = this.onChange(
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The variable name disposable is inconsistent with the naming in the onChange method which also uses disposable. Consider renaming this to depDisposable or dependencyDisposable to clarify it's a disposable for a dependency listener, distinguishing it from the general disposable in the onChange method.

Copilot uses AI. Check for mistakes.
dep,
this.batchAddListener(() => {
newData.useResolve = trackConditionalResolve
Expand All @@ -556,6 +559,7 @@ export class DataStore<T extends DataStoreType = "CTX"> {
}
}, dataConf)
);
this.addDisposable(dataConf.name, disposable);
}
}

Expand All @@ -579,6 +583,15 @@ export class DataStore<T extends DataStoreType = "CTX"> {
return true;
}

private addDisposable(name: string, disposable: Dispose) {
const existingDisposables = this.disposableMap.get(name);
if (existingDisposables) {
existingDisposables.push(disposable);
} else {
this.disposableMap.set(name, [disposable]);
}
}

/**
* For sub-routes to be incrementally rendered,
* dispose their data and pending tasks.
Expand All @@ -589,6 +602,13 @@ export class DataStore<T extends DataStoreType = "CTX"> {
if (names !== undefined) {
for (const name of names) {
this.data.delete(name);
const disposables = this.disposableMap.get(name);
if (disposables) {
for (const disposable of disposables) {
disposable();
}
Copy link

Copilot AI Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disposables array cleanup is missing. After calling all dispose functions, the array should be cleared (e.g., disposables.length = 0) before deleting the map entry, similar to the pattern used in bindListeners.ts:72 and mount.ts:139. While the map entry is deleted, this could leave dangling references if the array is still referenced elsewhere.

Suggested change
}
}
disposables.length = 0;

Copilot uses AI. Check for mistakes.
}
this.disposableMap.delete(name);
}
this.routeMap.delete(route);
}
Expand Down
6 changes: 2 additions & 4 deletions packages/runtime/src/internal/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,6 @@ export type RememberedEventListener = [string, EventListener];

export interface RuntimeBrickElement extends HTMLElement {
$$typeof?: "brick" | "provider" | "custom-template" | "native" | "invalid";
/** Meta info of listeners, for devtools only */
$$eventListeners?: MetaInfoOfEventListener[];
/** Remembered listeners for unbinding */
$$listeners?: RememberedEventListener[];
/** Remembered proxy listeners for unbinding */
$$proxyListeners?: RememberedEventListener[];
/** Find element by ref in a custom template */
Expand Down Expand Up @@ -209,3 +205,5 @@ export interface RouteNode {
// All ordered sibling routes under the same parent including the route itself
routes: RouteConf[];
}

export type Dispose = () => void;
15 changes: 13 additions & 2 deletions packages/runtime/src/internal/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { bindListeners } from "./bindListeners.js";
import { setRealProperties } from "./compute/setRealProperties.js";
import { RenderTag } from "./enums.js";
import type {
Dispose,
RenderReturnNode,
RenderRoot,
RuntimeBrickElement,
Expand All @@ -16,11 +17,12 @@ export function unmountTree(mountPoint: HTMLElement | DocumentFragment) {
export function mountTree(
root: RenderRoot,
initializedElement?: RuntimeBrickElement
): void {
): Dispose {
root.mounted = true;
window.DISABLE_REACT_FLUSH_SYNC = false;
let current = root.child;
const portalElements: RuntimeBrickElement[] = [];
const disposables: Dispose[] = [];
while (current) {
current.mounted = true;
if (current.tag === RenderTag.BRICK) {
Expand Down Expand Up @@ -56,7 +58,10 @@ export function mountTree(
current.tplHostMetadata.tplStateStoreId;
}
setRealProperties(element, current.properties);
bindListeners(element, current.events, current.runtimeContext);
disposables.push(
bindListeners(element, current.events, current.runtimeContext)
);

if (current.tplHostMetadata) {
// 先设置属性,再设置 `$$tplStateStore`,这样,当触发属性设置时,
// 避免初始化的一次 state update 操作及其 onChange 事件。
Expand Down Expand Up @@ -127,4 +132,10 @@ export function mountTree(
setTimeout(() => {
window.DISABLE_REACT_FLUSH_SYNC = true;
});
return () => {
for (const dispose of disposables) {
dispose();
}
disposables.length = 0;
};
}
6 changes: 5 additions & 1 deletion packages/runtime/src/internal/secret_internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
RuntimeDataValueOption,
RenderBrick,
RenderChildNode,
Dispose,
} from "./interfaces.js";
import { mountTree, unmountTree } from "./mount.js";
import { RenderTag } from "./enums.js";
Expand Down Expand Up @@ -132,6 +133,7 @@ export async function renderUseBrick(

export interface MountUseBrickResult {
portal?: HTMLElement;
dispose: Dispose;
}

export function mountUseBrick(
Expand All @@ -148,7 +150,7 @@ export function mountUseBrick(
return portal;
};

mountTree(renderRoot, element);
const dispose = mountTree(renderRoot, element);

rendererContext.dispatchOnMount();
rendererContext.initializeScrollIntoView();
Expand All @@ -161,6 +163,7 @@ export function mountUseBrick(

return {
portal,
dispose,
};
}

Expand All @@ -171,6 +174,7 @@ export function unmountUseBrick(
// if (mountResult.mainBrick) {
// mountResult.mainBrick.unmount();
// }
mountResult.dispose();
if (mountResult.portal) {
unmountTree(mountResult.portal);
mountResult.portal.remove();
Expand Down
Loading