import React$1, { ReactNode, FC, HTMLElementType, ComponentType, CSSProperties, ReactElement, Ref, Dispatch } from 'react';
interface SwitchProps {
value: T;
/**
* @description 可选的比对函数,默认() => a===b
* @description_en optional compare function, default to () => a === b
**/
compare?: (a: T, b: T) => boolean;
children?: React$1.ReactNode;
/**
* @description 是否严格模式,默认 false.建议跟随开发环境变化,严格模式下,会循环所有节点来提供更多的错误提示
* @description_en strict mode, default to false. It is recommended to follow the development environment changes. In strict mode, all nodes will be looped to provide more error prompts
* @extra
* 1.严格模式下,无论什么情况都会循环所有节点,以确保所有的 case 和 default 都能被检查到
* 2.非严格模式下,如果有一个 case 匹配成功,就不会继续循环
* @extra_en
* 1. In strict mode, all nodes will be looped regardless of the situation to ensure that all cases and defaults can be checked
* 2. In non-strict mode, if a case matches successfully, it will not continue to loop
*/
strict?: boolean;
}
interface SwitchCaseProps {
value: T;
children?: React$1.ReactNode;
}
interface SwitchDefaultProps {
children?: React$1.ReactNode;
}
/**
* @description Switch 组件用于根据传入的 value 渲染不同的子组件
* @description_en The Switch component is used to render different child components based on the passed value
*/
declare const Switch: {
(props: SwitchProps): React$1.ReactElement | null;
displayName: string;
Case: {
(props: SwitchCaseProps): React$1.ReactElement;
displayName: string;
};
Default: {
(props: SwitchDefaultProps): React$1.ReactElement;
displayName: string;
};
createTyped(): {
Switch: (props: SwitchProps) => React$1.ReactElement | null;
Case: (props: SwitchCaseProps) => React$1.ReactElement;
Default: (props: SwitchDefaultProps) => React$1.ReactElement;
};
};
interface IfProps {
condition: boolean;
children?: React$1.ReactNode;
}
interface ThenProps {
children?: React$1.ReactNode;
}
interface ElseProps {
children?: React$1.ReactNode;
}
interface ElseIfProps {
condition: boolean;
children?: React$1.ReactNode;
}
declare const If: {
({ condition, children, }: IfProps): React$1.ReactElement | null;
displayName: string;
Then: React$1.FC;
ElseIf: React$1.FC;
Else: React$1.FC;
createTyped(): {
If: (props: IfProps) => React$1.ReactElement | null;
Then: (props: ThenProps) => React$1.ReactElement;
ElseIf: (props: ElseIfProps) => React$1.ReactElement;
Else: (props: ElseProps) => React$1.ReactElement;
};
};
interface TrueProps {
condition: boolean;
children?: ReactNode;
}
declare const True: FC;
interface FalseProps {
condition: boolean;
children?: ReactNode;
}
declare const False: FC;
interface WhenProps {
/**
* @description_en An array of conditions where all must be true to render children.
* @description_zh 一个条件数组,所有条件都为真时渲染子元素。
* @index 1
*/
all?: boolean[];
/**
* @description_en An array of conditions where at least one must be true to render children.
* @description_zh 一个条件数组,至少有一个条件为真时渲染子元素。
* @index 2
*/
any?: boolean[];
/**
* @description_en An array of conditions where all must be false to render children.
* @description_zh 一个条件数组,所有条件都为假时渲染子元素。
* @index 3
*/
none?: boolean[];
/**
* @description_en The content to render when conditions are satisfied.
* @description_zh 满足条件时渲染的内容。
*/
children?: ReactNode;
/**
* @description_en The content to render when conditions are not satisfied (alternative to `Else`).
* @description_zh 不满足条件时渲染的内容(替代 `Else`)。
*/
fallback?: ReactNode;
}
/**
* @description_zh 一个声明式组件,用于基于多个条件进行条件渲染。比If更简洁。
* @description_en A declarative component for conditional rendering based on multiple conditions. More concise than If.
* @component
* @example
* ```tsx
*
*
*
* ```
*
* @example
* ```tsx
* }>
*
*
* ```
*
*/
declare const When: FC;
/**
* Props for the `Pipe` component.
*
* @interface PipeProps
* @property {any} data - The initial data to process.
* @property {((input: any) => any)[]} transform - Array of functions to transform the data.
* @property {(result: any) => ReactNode} render - Function to render the transformed data.
* @property {ReactNode} [fallback] - Content to render if the result is null or undefined.
*/
interface PipeProps {
/**
* @description_en The initial data to process.
* @description_zh 要处理的初始数据。
*/
data: any;
/**
* @description_en Array of functions to transform the data.
* @description_zh 转换数据的函数数组。
*/
transform: ((input: any) => any)[];
/**
* @description_en Function to render the transformed data.
* @description_zh 渲染转换后数据的函数。
*/
render: (result: any) => ReactNode;
/**
* @description_en Content to render if the result is null or undefined.
* @description_zh 如果结果为 null 或 undefined 时渲染的内容。
* @optional
* @default null
*/
fallback?: ReactNode;
}
/**
* @description_zh 一个声明式组件,用于通过管道式处理数据并渲染,简化多步骤数据转换。
* @description_en A declarative component for processing data through a pipeline and rendering, simplifying multi-step data transformations.
* @component
* @example
* ```tsx
* data.filter(user => user.active),
* (data) => data.map(user => user.name)
* ]}
* render={(names) =>
{names.join(", ")}
}
* fallback={
No Data
}
* />
* ```
*/
declare const Pipe: FC;
interface SizeBoxProps {
size?: number | string;
height?: number | string;
width?: number | string;
h?: number | string;
w?: number | string;
children?: ReactNode;
className?: string;
}
/**
* @description SizeBox 组件用于设置一个固定大小的盒子
*/
declare const SizeBox: FC;
/**
* Props for the `Scope` component.
*
* @interface ScopeProps
* @property {Record | ((props: any) => Record)} let - An object or function defining local variables for the scope.
* @property {any} [props] - Optional props passed to the let function.
* @property {(scope: Record) => ReactNode} [children] - Render function to access the scope variables.
* @property {ReactNode} [fallback] - Content to render if children or scope is empty.
*/
interface ScopeProps {
/**
* @description_en An object or function defining local variables for the scope.
* @description_zh 定义作用域局部变量的对象或函数。
*/
let: Record | ((props: any) => Record);
/**
* @description_en Optional props passed to the let function.
* @description_zh 传递给 let 函数的可选属性。
* @optional
*/
props?: any;
/**
* @description_en Render function to access the scope variables.
* @description_zh 访问作用域变量的渲染函数。
* @optional
*/
children?: (scope: Record) => ReactNode;
/**
* @description_en Content to render if children or scope is empty.
* @description_zh 如果 children 或 scope 为空时渲染的内容。
* @optional
* @default null
*/
fallback?: ReactNode;
}
/**
* @description_zh 一个声明式组件,为子节点提供局部作用域,简化临时状态或上下文管理。
* @description_en A declarative component that provides a local scope for children, simplifying temporary state or context management.
* @component
* @example
* ```tsx
*
* {({ count, text }) =>
}
*
* ```
*/
declare function DateRender({ source, format, children, }: DateRenderProps): React$1.JSX.Element | null;
/**
* @zh `useAppStack()` 返回的导航 API。每个方法绑定到最近的 `AppStackRouter` 实例。
* @en Navigation API returned by `useAppStack()`. Each method is bound to the nearest `AppStackRouter`.
*/
interface AppStackApi {
/**
* @zh 压入一个新屏幕到栈顶。`params` 会作为 props 透传给该组件。
* @en Push a new screen onto the top. `params` are forwarded as props.
*/
push:
(Component: ComponentType
, params?: P) => void;
/**
* @zh 弹出栈顶屏幕。栈空时为空操作。
* @en Pop the top screen. No-op when empty.
*/
pop: () => void;
/**
* @zh 替换栈顶屏幕,不改变栈深度。空栈时退化为 push。
* @en Replace the top screen without changing depth. Degrades to push on an empty stack.
*/
replace:
(Component: ComponentType
, params?: P) => void;
/**
* @zh 清空整个堆栈,回到根屏幕(无过渡动画)。
* @en Clear the entire stack, returning to the root screen (no transition).
*/
reset: () => void;
/**
* @zh 当前栈是否可出栈(深度 > 0)。
* @en Whether the stack can be popped (depth > 0).
*/
canPop: () => boolean;
/**
* @zh 当前栈深度(不含根屏幕)。
* @en Current stack depth (excludes the root screen).
*/
size: number;
}
/**
* @zh 在 `AppStackRouter` 内部任意子组件中获取导航 API。在 Router 外调用会抛错。
*
* 注意:`size` 与 `canPop()` 会在栈深度变化时同步更新(内部仅订阅深度,不订阅完整栈,
* 因此栈内容变化但深度不变时不会触发重渲染,比订阅完整栈更高效)。
* 如需在栈内容变化时重渲染(罕见),使用 `useStackSize` 或直接读取 `size`。
* @en Obtain the navigation API from any descendant of `AppStackRouter`. Throws when used outside.
*
* Note: `size` and `canPop()` stay in sync with the stack depth (subscribed to depth only, not the
* full stack, so content changes that don't alter depth won't trigger a re-render -- more efficient
* than subscribing to the full stack).
*/
declare function useAppStack(): AppStackApi;
/**
* @zh 响应式订阅栈深度(不含根屏幕)。仅深度变化时重渲染。
* @en Reactively subscribe to the stack depth (excludes the root screen). Re-renders only on depth change.
*/
declare function useStackSize(): number;
/**
* @zh 响应式订阅当前栈是否可出栈(深度 > 0)。仅 canPop 状态变化时重渲染。
* @en Reactively subscribe to whether the stack can be popped (depth > 0). Re-renders only when the
* canPop state changes.
*/
declare function useCanPop(): boolean;
interface AppStackRouterProps {
/**
* @description_zh 根屏幕,始终位于栈底并被渲染。刷新后回到这里。
* @description_en Root screen, always at the bottom and rendered. The app returns here on refresh.
*/
root: React$1.ReactElement;
/**
* @description_zh 最大栈深度,超过则丢弃最底层屏幕以释放内存。默认无限制。
* @description_en Max stack depth; the bottom screen is dropped when exceeded. Default unlimited.
* @optional
*/
maxStackSize?: number;
/**
* @description_zh 是否启用边缘左滑返回手势。
* @description_en Whether the edge swipe-back gesture is enabled.
* @default true
*/
swipeBack?: boolean;
/**
* @description_zh 触发拖拽的左边缘宽度(px)。
* @description_en Left-edge width (px) that starts a drag.
* @default 40
*/
swipeBackEdgeWidth?: number;
/**
* @description_zh 松手时若手指正朝"取消"方向(向左)运动,则强制回弹、不出栈,
* 即使当前位移已超过阈值。设为 false 则仅按距离/速度判定。典型场景:向右划出后反悔、
* 向左划回,松手时位置仍在阈值之上但意图是取消返回。
* @description_en When the finger is moving towards the "cancel" direction (leftwards) on release,
* force a snap-back instead of committing a pop, even if the offset already exceeds the threshold.
* Set to false to judge only by distance/velocity. Typical scenario: drag out to the right, change
* your mind and drag back left; on release the position is still above the threshold but the intent
* is to cancel.
* @default true
*/
swipeBackCancelOnReverseRelease?: boolean;
/**
* @description_zh 判定"朝取消方向运动"的最小瞬时速度(px / ms)。
* @description_en Minimum instantaneous velocity (px/ms) to count as a clear "cancel" motion.
* @default 0.1
*/
swipeBackCancelVelocity?: number;
/**
* @description_zh 是否启用安全区域(env(safe-area-inset-*))内边距。
* @description_en Whether to apply safe-area (env(safe-area-inset-*)) padding.
* @default true
*/
safeArea?: boolean;
/**
* @description_zh 进出场过渡时长(ms)。设为 0 可禁用过渡。
* @description_en Enter/exit transition duration (ms). Set to 0 to disable.
* @default 300
*/
transitionDuration?: number;
/**
* @description_zh 是否以视口高度(100dvh)撑满整个屏幕。移动端单页应用建议保持开启;
* 若需嵌入到已具备高度的父容器内,可设为 false(此时容器高度为 100%,依赖父级高度)。
* @description_en Whether to fill the whole screen via viewport height (100dvh). Recommended on for
* mobile SPAs; set to false to embed inside a parent that already has a height (the container then
* uses 100% height, depending on the parent).
* @default true
*/
fullscreen?: boolean;
/**
* @description_zh 容器 className。
* @description_en Container className.
* @optional
*/
className?: string;
/**
* @description_zh 容器内联样式。
* @description_en Container inline style.
* @optional
*/
style?: CSSProperties;
/**
* @description_zh 渲染在堆栈之上的全局叠层(如 toast)。不受手势/过渡影响。
* @description_en Global overlay rendered above the stack (e.g. toast). Unaffected by gestures/transitions.
* @optional
*/
children?: ReactNode;
}
/**
* @description_zh 移动端 H5 单页应用风格的堆栈视图容器。屏幕切换后被压入堆栈并 keep-alive(不卸载),
* 支持编程式导航、边缘左滑返回、拦截浏览器返回键、安全区域适配。不引入任何额外依赖。
*
* 使用前建议在 HTML 中设置 ``
* 以让 `env(safe-area-inset-*)` 生效。
*
* @description_en A mobile-app-style stack view container for H5 SPAs. Screens are pushed onto a
* stack and kept alive (not unmounted). Supports programmatic navigation, edge swipe-back, browser
* back-button interception, and safe-area adaptation. Zero extra dependencies.
*
* For `env(safe-area-inset-*)` to take effect, set
* `` in HTML.
* @component
* @example
* ```tsx
* function Home() {
* const { push } = useAppStack()
* return
* }
*
* function Profile({ id }: { id: number }) {
* const { pop } = useAppStack()
* return
* }
*
* } />
* ```
*/
declare function AppStackRouter({ root, maxStackSize, swipeBack, swipeBackEdgeWidth, swipeBackCancelOnReverseRelease, swipeBackCancelVelocity, safeArea, transitionDuration, fullscreen, className, style, children, }: AppStackRouterProps): ReactNode;
declare namespace AppStackRouter {
var displayName: string;
}
/**
* @zh 合帧投递:把 props 更新合并为每个帧率窗口至多一次提交。
*
* 它不是"节流渲染",而是"合帧投递"。设间隔 `T = 1000 / fps`,对任意窗口 `[t, t+T)`:
*
* 1. 子组件收到的 props 至多更新一次,且携带窗口内最后一次更新的值(最新值胜出);
* 2. 若新值与已提交值按 `compare` 判定相等,则不发生任何提交 —— 子组件继续收到上一次的
* 同一个元素引用,React 在 `memo` 边界直接 bail out;
* 3. 未提交期间父组件传入的新 props 只进控制器,绝不进入子组件的 props。
*
* 父组件可以以任意高的频率重渲染:本组件自身每次都会重新执行(一次 O(1)、零分配的捕获),
* 但昂贵的子树只在帧边界上渲染一次。这正是"抽离状态层"的收益所在 —— 高频状态放在本组件
* 外层,昂贵的渲染留在内层。
*
* 它由两个已有工具组合而成:`rafSchedule` 当泵、`createLatestValue` 当合并槽。
*
* @en Framed delivery: coalesce prop updates into at most one commit per frame window.
*
* This is not "throttled rendering" but framed delivery. With `T = 1000 / fps`, for any window
* `[t, t+T)`:
*
* 1. the child receives at most one props update, carrying the LAST value of the window
* (latest wins);
* 2. if the new value compares equal to the committed one (per `compare`), no commit happens at
* all — the child keeps receiving the same element reference and React bails out at the
* `memo` boundary;
* 3. updates arriving before a commit reach the controller only, never the child's props.
*
* The parent may re-render arbitrarily often: this component re-runs every time (one O(1),
* allocation-free capture), but the expensive subtree renders only at frame boundaries. That is
* why extracting the state layer pays off — keep the high-frequency state OUTSIDE and the
* expensive rendering INSIDE.
*
* It is assembled from two existing utilities: `rafSchedule` as the pump and `createLatestValue`
* as the merge cell.
*/
/**
* @zh 泵的传输层策略。
* - `'auto'`(默认):可见时用 `requestAnimationFrame`,隐藏时(仅在 `pauseWhenHidden` 为
* false 时才会走到)退化为定时器。
* - `'raf'`:始终用 `requestAnimationFrame`。与绘制对齐,但标签页隐藏时不会触发。
* - `'timer'`:始终用 `setTimeout`,节拍与绘制解耦。
*
* @en Transport strategy for the pump.
* - `'auto'` (default): `requestAnimationFrame` while visible, falling back to a timer while
* hidden (only reachable when `pauseWhenHidden` is false).
* - `'raf'`: always `requestAnimationFrame` — aligned with painting, but it does not fire while
* the tab is hidden.
* - `'timer'`: always `setTimeout`, decoupled from painting.
*/
type FrameStrategy = 'auto' | 'raf' | 'timer';
/**
* @zh 可注入的帧调度器:接收回调,返回取消函数。默认是 `requestAnimationFrame`。注入它既能
* 替换传输层(例如改用 `requestIdleCallback`),也让单元测试可以确定性地推进帧而不依赖真实
* 计时。
*
* @en Injectable frame scheduler: receives a callback and returns a cancel function. Defaults to
* `requestAnimationFrame`. Injecting one both replaces the transport (e.g. to use
* `requestIdleCallback`) and lets unit tests advance frames deterministically.
*/
type FrameScheduler = (callback: (time: number) => void) => () => void;
/**
* @zh 相等判定策略。返回 true 表示"相等",即跳过本次提交。
* - `'shallow'`(默认):逐键 `Object.is` 浅比较;
* - `'reference'`:只比引用。最便宜,但父组件每次新建的字面量对象都会判定为不等,等于每个
* 窗口都提交;
* - `'never'`:永不相等,即每次泵触发都提交;
* - 自定义函数:自行决定,适合用一个廉价的 version 字段代替深比较。
*
* @en Equality strategy. True means "equal", i.e. skip this commit.
* - `'shallow'` (default): per-key `Object.is` shallow compare;
* - `'reference'`: reference only. Cheapest, but a literal object rebuilt by the parent on every
* render always compares unequal, so every window commits;
* - `'never'`: never equal — commit on every pump tick;
* - custom function: your call — handy for comparing a cheap version field instead of deep
* comparing.
*/
type FrameCompare = 'shallow' | 'reference' | 'never' | ((prev: unknown, next: unknown) => boolean);
/**
* @zh `onDrop` 的丢弃原因。只报告"有意义的丢弃":被更新值覆盖(合帧的正常工况)不计入 ——
* 一个窗口内 10 次更新有 9 次被覆盖,逐条上报只会是噪音,那个数字看统计里的 `coalesced`。
*
* @en Why an update was dropped. Only meaningful drops are reported; being superseded by a newer
* value is the normal operation of coalescing and is not reported — 9 of 10 updates in a window
* are superseded routinely, and reporting each would be pure noise. Read `coalesced` instead.
*/
type FrameDropReason = 'vetoed' | 'cancelled' | 'unmounted';
/**
* @zh 传给各生命周期钩子的上下文快照。
* @en Context snapshot handed to the lifecycle callbacks.
*/
interface FrameRenderContext {
/**
* @zh 本阶段的时间戳,与 rAF 回调同一时间线(`performance.now()` 口径)。
* @en Timestamp of this stage, on the same timeline as rAF callbacks (`performance.now()`).
*/
time: number;
/**
* @zh 泵触发过多少次(含最终没有提交的帧)。
* @en Pump ticks so far, including ticks that did not commit.
*/
frame: number;
/**
* @zh 已提交次数。
* @en Commits so far.
*/
commits: number;
/**
* @zh 被合帧抑制的父渲染次数。
* @en Parent renders suppressed by coalescing.
*/
coalesced: number;
/**
* @zh 当前目标帧率。
* @en Current target frame rate.
*/
fps: number;
}
/**
* @zh `getStats()` 的返回结构,自挂载起累计。
* @en Shape returned by `getStats()`, accumulated since mount.
*/
interface FrameRenderStats {
/**
* @zh 泵触发次数。
* @en Pump ticks.
*/
frames: number;
/**
* @zh 捕获到的父渲染次数。自身提交引起的重渲染不计入 —— 那种渲染没有带来新东西。
* @en Captured parent renders. Re-renders caused by our own commits are not counted — they
* brought nothing new.
*/
captures: number;
/**
* @zh 实际提交次数 —— 子组件渲染次数的上界。
* @en Actual commits — an upper bound on child renders.
*/
commits: number;
/**
* @zh `compare` 判定相等而跳过的次数,这部分完全不渲染。
* @en Skips where `compare` found the value equal — no render at all.
*/
skips: number;
/**
* @zh `shouldCommit` 否决的次数。
* @en Vetoes by `shouldCommit`.
*/
vetoes: number;
/**
* @zh 被合帧抑制的父渲染次数 —— 收益的直接度量。
* @en Parent renders suppressed by coalescing — the direct measure of the win.
*/
coalesced: number;
/**
* @zh 真正被丢弃的次数(否决 + 取消 + 卸载)。
* @en Truly dropped updates (veto + cancel + unmount).
*/
dropped: number;
/**
* @zh 最近若干次提交实测出的提交频率。
* @en Commit rate measured over the most recent commits.
*/
commitsPerSecond: number;
}
/**
* @zh 命令式句柄,通过 `ref` 获取。
* @en Imperative handle, obtained through `ref`.
*/
interface FrameRenderHandle {
/**
* @zh 立即提交待处理的最新值,跳过时间门。用于"这一刻必须新鲜"的场景:用户点击刷新、
* 导出快照、提交表单前。返回是否真的发生了提交(无待处理值或判定相等时为 false)。
* @en Commit the latest pending value immediately, bypassing the time gate. For moments that
* must be fresh: a refresh click, exporting a snapshot, right before submitting a form.
* Returns whether a commit actually happened (false when nothing is pending or the value
* compares equal).
*/
flush(): boolean;
/**
* @zh 丢弃待处理值并取消已预约的帧。返回是否丢弃了东西。
* @en Drop the pending value and cancel the booked frame. Returns whether anything was dropped.
*/
cancel(): boolean;
/**
* @zh 暂停提交,待处理值保留。
* @en Pause committing; the pending value is kept.
*/
pause(): void;
/**
* @zh 恢复提交;若有待处理值,下一帧提交最新值。
* @en Resume committing; with a pending value, the latest value commits on the next frame.
*/
resume(): void;
/**
* @zh 读取统计快照。用它验证收益,而不是凭感觉。
* @en Read a stats snapshot. Verify the win instead of guessing.
*/
getStats(): FrameRenderStats;
}
/**
* @zh `FrameRender` 的 props。泛型 `P` 是子组件的 props 类型:element 形式下由元素推断,
* 函数形式下由 `props` 与该函数的签名共同决定。
* @en `FrameRender` props. Generic `P` is the child's props type: inferred from the element in
* element form, or from `props` together with the render function's signature.
*/
interface FrameRenderProps
> {
/**
* @zh 要合帧投递的子元素,或 `(props) => ReactNode` 渲染函数。必须是单个元素:Fragment、
* 数组、字符串无法提取 props,会报警告并旁路直通。
*
* element 形式最顺手,但注意**窗口内子组件渲染的是上一次的 props** —— 这就是换取吞吐的
* 代价。函数形式适合需要派生的场景,且不存在"陈旧 children"的歧义。
* @en The child element to deliver frame by frame, or a `(props) => ReactNode` render function.
* Must be a single element: a Fragment, array or string has no extractable props, so it warns
* and passes through.
*
* The element form reads best, but note that **inside a window the child renders the previous
* props** — that is the price of throughput. The function form suits cases needing derivation
* and has no "stale children" ambiguity.
*/
children: ReactElement
| ((props: P) => ReactNode);
/**
* @zh 函数形式的 props 包(element 形式不需要)。省略时函数收到 `{}`。
* @en The props bag for the function form (not needed in element form). Defaults to `{}`.
*/
props?: P;
/**
* @zh 目标提交帧率。`T = 1000 / fps`,提交间隔不小于 `T`(含 4ms 相位容差,避免间隔与帧
* 周期相等时掉到一半帧率)。超过屏幕刷新率没有意义,有效上限就是刷新率。传 `<= 0` 表示旁路
* (每次父渲染都提交),用于和正常模式对比收益。
* @default 60
* @en Target commit frame rate. `T = 1000 / fps`, commits at least `T` apart (with a 4ms phase
* tolerance so an interval equal to the frame period does not halve the rate). Exceeding the
* display refresh rate achieves nothing — the refresh rate is the ceiling. `<= 0` bypasses
* framing (commit on every parent render) for A/B-ing the win.
*/
fps?: number;
/**
* @zh 泵的传输层策略。
* @default 'auto'
* @en Transport strategy for the pump.
*/
strategy?: FrameStrategy;
/**
* @zh 窗口内首个更新是否尽快提交(下一帧,不是同步)。置 false 则首个提交需要等满一个间隔。
* 与 throttle 的 leading 同义。
* @default true
* @en Whether the first update of a window commits as soon as possible (next frame, not
* synchronously). False makes the first commit wait a full interval. Same as throttle leading.
*/
leading?: boolean;
/**
* @zh 窗口内的更新是否算数。置 false 为采样语义:只有落到窗口边界上的那次更新会提交,窗口
* 内的更新直接忽略。注意 `leading` 与 `trailing` 同为 false 时几乎不会提交。
* @default true
* @en Whether updates inside a window count. False means sampling: only the update landing on a
* window boundary commits, and in-window updates are ignored. With both `leading` and `trailing`
* false, commits become nearly impossible.
*/
trailing?: boolean;
/**
* @zh 暂停提交,待处理值保留;恢复时下一帧提交最新值。适合手势或弹窗打开期间冻结更新。
* @default false
* @en Pause committing while keeping the pending value; on resume the latest value commits on the
* next frame. Handy for freezing updates during a gesture or an open modal.
*/
paused?: boolean;
/**
* @zh 完全旁路:不调度、不比较、不统计,每次父渲染直接透传。用于 A/B 对比合帧收益,也用于
* 排查"是不是合帧导致的问题"。等价于 `fps <= 0`。
* @default false
* @en Full bypass: no scheduling, no comparison, no stats — every parent render passes straight
* through. For A/B-ing the win, or bisecting "is framing causing this?". Equivalent to
* `fps <= 0`.
*/
disabled?: boolean;
/**
* @zh 标签页隐藏时不做提交。这是性能组件的正确默认值:不可见的提交是纯浪费,且 rAF 在隐藏时
* 本就不触发。注意它会把 `onCommit` 这类副作用的时序推迟到标签页重新可见。
*
* 还有一条浏览器行为要知道:`strategy` 为 `'auto'` / `'raf'` 时泵走 rAF,而浏览器不只在
* `visibilityState === 'hidden'` 时暂停 rAF —— 窗口被其他窗口完全遮挡时同样会暂停。那种情况下
* 也不会发生提交,效果与暂停等同,窗口回到前台后立即恢复。若需要在被遮挡时仍保持提交节拍,
* 用 `strategy: 'timer'`。
* @default true
* @en Do not commit while the tab is hidden. The right default for a performance component:
* invisible commits are pure waste, and rAF does not fire while hidden. Note that this defers
* side effects such as `onCommit` until the tab is visible again.
*
* One browser behaviour is worth knowing too: with `strategy` set to `'auto'` / `'raf'` the pump
* rides on rAF, and the browser suspends rAF not only when `visibilityState === 'hidden'` but also
* when the window is fully covered by another window. In that state nothing commits either — the
* effect equals a pause — and it resumes as soon as the window comes back. If you need the commit
* cadence to hold while covered, use `strategy: 'timer'`.
*/
pauseWhenHidden?: boolean;
/**
* @zh 注入自定义帧调度器,替代 `strategy`。也用于让测试确定性地推进帧。
* @en Inject a custom frame scheduler, replacing `strategy`. Also used to advance frames
* deterministically in tests.
*/
scheduler?: FrameScheduler;
/**
* @zh 只影响**比较**,不影响子组件收到的 props。返回参与比较的字段子集 —— 子组件仍然拿到完整
* 的 props,但只有选出的字段变化才会触发提交。这是消除"每次渲染都新建的回调 / 字面量"造成
* 无谓提交的关键。
* @en Affects the **comparison** only, never what the child receives. Return the subset of fields
* that participate in comparison — the child still gets the full props, but only a change in the
* selected fields triggers a commit. The key to eliminating pointless commits caused by
* per-render callbacks and object literals.
*/
select?: (props: P) => unknown;
/**
* @zh 相等判定策略,返回 true 表示相等、跳过提交。
* @default 'shallow'
* @en Equality strategy; true means equal, so the commit is skipped.
*/
compare?: FrameCompare;
/**
* @zh 提交前的策略否决。返回 false 则丢弃本次待处理值(计入 `vetoes`,并经 `onDrop` 以
* `'vetoed'` 上报)。例如:标签页隐藏时不做无用渲染、拖拽中冻结、或业务上判定"无实质变化"。
*
* 参数是 `select` 之后的值,与 `compare` 看到的一致。
* @en Policy veto before committing. False drops the pending value (counted in `vetoes` and
* reported through `onDrop` as `'vetoed'`) — e.g. skip invisible work while hidden, freeze during
* a drag, or drop business-insignificant changes.
*
* Arguments are the post-`select` values, the same ones `compare` sees.
*/
shouldCommit?: (prev: unknown, next: unknown, ctx: FrameRenderContext) => boolean;
/**
* @zh 每次泵触发都调用(含最终没有提交的帧)。**不是**通用帧时钟:只有存在待处理更新时才会
* 泵,空闲时一帧都不跑 —— 这也正是它省电的原因。需要连续帧回调请直接用 `rafSchedule`。
* @en Called on every pump tick, including ticks that end up not committing. This is **not** a
* general frame clock: the pump only runs while an update is pending, and runs zero frames when
* idle — which is exactly why it is cheap. For a continuous frame callback use `rafSchedule`.
*/
onFrame?: (ctx: FrameRenderContext) => void;
/**
* @zh 提交之后在 effect 中调用(绝不在渲染期间)。用于埋点、同步外部系统、记录指标。
* @en Called after a commit, inside an effect (never during render). For instrumentation, syncing
* external systems, recording metrics.
*/
onCommit?: (props: P, ctx: FrameRenderContext) => void;
/**
* @zh 发生"有意义的丢弃"时调用:被否决、被取消、卸载时仍有待处理值。被更新值覆盖(合帧的
* 正常工况)不上报。
* @en Called for meaningful drops: vetoed, cancelled, or still pending at unmount. Superseding
* (the normal operation of coalescing) is not reported.
*/
onDrop?: (props: P, reason: FrameDropReason, ctx: FrameRenderContext) => void;
/**
* @zh 是否在开发环境打印一次"只适合无状态子组件"的提示。默认开启;置 false 关闭。
* @default true
* @en Whether to print the "stateless children only" notice once in development. On by default;
* pass false to silence it.
*/
warn?: boolean;
}
/**
* @zh 合帧投递组件。详见模块头与 {@link FrameRenderProps}。
*
* @example
* ```tsx
* // 状态在外层(高频无所谓),昂贵的子树每帧只渲染一次
* function Dashboard() {
* const ticks = useHighFrequencyTicks()
* return (
*
*
*
* )
* }
*
* // 渲染函数形式:需要派生时用,props 包是被合帧投递的对象
*
* {({data, theme}) => }
*
* ```
*
* @en Framed delivery component. See the module header and {@link FrameRenderProps}.
*/
declare const FrameRender:
>(props: FrameRenderProps
& {
ref?: Ref;
}) => ReactElement | null;
interface UseControlledOptions {
/**
* @description - 非受控模式下的默认值,会被受控模式下的值覆盖
* @description_en - Default value in uncontrolled mode, will be overridden by the value in controlled mode
*/
defaultValue: T;
/**
* @description - 值变更前的回调函数,可用于拦截或修改新值
* @description_en - Callback function before the value changes, can be used to intercept or modify the new value
*/
onBeforeChange?: (newValue: T, currentValue: T) => boolean | void;
/**
* @description - 当值发生变化时触发的回调函数名
* @description_en - Callback function name triggered when the value changes
* @default - onChange
*/
trigger?: string;
/**
* @description - 值的属性名
* @description_en - Property name of the value
* @default - value
*/
valuePropName?: string;
props: Record;
}
declare function useControlled(options: UseControlledOptions): [T, Dispatch>];
/**
* @en Disposal primitives for the library's push-based APIs ({@link Emitter}, {@link Event},
* {@link DisposableStore}).
*
* The protocol is a plain `dispose()` method — the shape VS Code, RxJS and monaco use — and every
* object built here *also* answers the real `Symbol.dispose` at runtime when the environment has it
* (Chrome 125+, Safari 18.4+, Firefox 134+, Node 20+), so `using` declarations and
* `Symbol.dispose` interop work without extra ceremony.
*
* The symbol is attached at runtime but deliberately kept out of the public *type*: this package
* ships `src/` and its `.d.ts`, and naming the global `Disposable` / `Symbol.dispose` in a
* signature makes `tsc` fail for consumers whose `lib` stops before `esnext.disposable`. It is the
* same reason {@link safePromiseTry} reaches for `Promise.try` through a cast instead of calling
* it: the library must not require a specific `lib` from everyone who imports it. Consumers who
* do have the newer lib can write `using sub = event(handler)` and get the cleanup for free.
*
* **No module-level side effects.** The symbol is installed from each *constructor*
* (`withDisposeSymbol(X.prototype)` is idempotent, so the first instance pays for it and the rest
* just hit the `in` check), never from a top-level statement. A top-level call makes the whole
* module undroppable for bundlers — a consumer importing `cx` would still ship the event system —
* and it is also what makes `"sideEffects": false` in `package.json` truthful. See the build
* section of AGENTS.md before moving one of these calls back out.
*
* @zh 推送式 API({@link Emitter}、{@link Event}、{@link DisposableStore})所需的可释放对象
* 基础设施。
*
* 协议就是一个普通的 `dispose()` 方法——与 VS Code、RxJS、monaco 同形——同时本模块产出的每个
* 对象在运行时支持的情况下还会响应真正的 `Symbol.dispose`(Chrome 125+、Safari 18.4+、
* Firefox 134+、Node 20+ 具备),因此 `using` 声明与 `Symbol.dispose` 互操作无需额外处理。
*
* 符号只在运行时挂上,刻意不写进公开*类型*:本库会发布 `src/` 与 `.d.ts`,在签名里引用全局
* `Disposable` / `Symbol.dispose` 会让 `lib` 停在 `esnext.disposable` 之前的使用者工程直接
* 编译报错——{@link safePromiseTry} 用类型断言而不是直接调用来取 `Promise.try`,是同一个理由。
* 而 lib 较新的使用者可以直接写 `using sub = event(handler)`,清理照常生效。
*
* **模块顶层不做任何调用。** 符号安装放在各自*构造函数*里(`withDisposeSymbol(X.prototype)`
* 是幂等的,第一个实例付这次开销,之后只走 `in` 判断),绝不写成顶层语句:顶层调用会让整个模块
* 无法被 bundler 丢弃——只 import `cx` 的使用者也会把事件系统打进产物——同时它也是
* `package.json` 里 `"sideEffects": false` 能成立的前提。要把这类调用挪回顶层之前,请先读
* AGENTS.md 的构建一节。
*/
/**
* @en A resource that is released by calling `dispose()`. Structurally compatible with TypeScript's
* built-in `Disposable` at runtime (see the module header).
* @zh 通过调用 `dispose()` 释放的资源。运行时与 TypeScript 内置的 `Disposable` 结构兼容
* (见模块头说明)。
*/
interface CompatDisposable {
dispose(): void;
}
/**
* @en Teach an object — or a class prototype — to answer `Symbol.dispose` by calling its own
* `dispose()`. No-op where the symbol does not exist. Returns the target so it can wrap a literal.
* @zh 让一个对象(或类原型)以自身的 `dispose()` 响应 `Symbol.dispose`。环境没有该符号时什么也
* 不做。返回传入对象,方便直接包住一个字面量。
*/
declare const withDisposeSymbol: (target: T) => T;
declare const noopDisposable: CompatDisposable;
/**
* @en Check whether `thing` is {@link CompatDisposable}. The arity check mirrors VS Code: an object
* with an unrelated `dispose(arg)` method should not be mistaken for a resource.
* @zh 判断 `thing` 是否为 {@link CompatDisposable}。形参个数检查来自 VS Code:名字叫 `dispose`
* 但带参数的无关方法不应该被当成可释放资源。
*/
declare function isDisposable(thing: E): thing is E & CompatDisposable;
/**
* @en Dispose every item of an iterable, collecting failures instead of stopping at the first one:
* one broken resource must not leave the rest un-released.
*
* A single failure is rethrown as-is; several are thrown as an `AggregateError` where the platform
* has it, and as the first failure elsewhere (the rest are still reported through `console.error`,
* so nothing is silently swallowed).
*
* @zh 释放一个可迭代对象里的每一项,并收集失败而不是在第一个错误处停下:一个坏掉的资源不该
* 让其余资源都留在那里。
*
* 只有一个失败时原样抛出;多个失败在平台支持时抛 `AggregateError`,否则抛第一个失败(其余仍会
* 通过 `console.error` 报出来,不会静默吞掉)。
*/
declare function disposeAll(disposables: Iterable): void;
/**
* @en Turn a cleanup function into a {@link CompatDisposable}. `fn` is guaranteed to run **once**
* (pass an arrow function if it needs `this`).
* @zh 把一个清理函数变成 {@link CompatDisposable}。`fn` 保证只执行**一次**(需要 `this` 时请传
* 箭头函数)。
*/
declare function toDisposable(fn: () => void): CompatDisposable;
/**
* @en Combine several disposables into one that releases all of them.
* @zh 把多个可释放对象合成一个,释放它即释放全部。
*/
declare function combinedDisposable(...disposables: CompatDisposable[]): CompatDisposable;
/**
* @en Manages a collection of disposables.
*
* Preferred over a bare `CompatDisposable[]` because it handles the edge cases: the same value can
* be added twice (the `Set` keeps one entry), and adding to an already-disposed store **warns and
* drops** the newcomer instead of registering it. Note the newcomer is *not* disposed for you (VS
* Code behaves the same way): the store cannot know whether the caller still holds it, so the choice
* stays with the caller — see `DisposableStore.DISABLE_DISPOSED_WARNING` to silence the warning.
*
* @example
* ```ts
* const store = new DisposableStore()
* store.add(emitter.event(handler))
* store.add(() => {}) // no — a function is not a disposable; wrap it in toDisposable()
* store.dispose() // everything above is released
* ```
*
* @zh 管理一组可释放对象。
*
* 比裸的 `CompatDisposable[]` 可靠,因为它处理了边界情况:同一个值可以重复添加(`Set` 只留一份),
* 往已释放的 store 里添加会**告警并丢弃**新来的对象,而不是登记它。注意它**不会**替你释放新来的
* 对象(VS Code 也是如此):store 无法判断调用方是否还持有它,这个选择留给调用方——要静默这条告警
* 见 `DisposableStore.DISABLE_DISPOSED_WARNING`。
*/
declare class DisposableStore implements CompatDisposable {
#private;
/**
* @en Set to `true` to silence the warning for adding to an already-disposed store (useful in
* tests that assert on the disposal path itself).
* @zh 置为 `true` 可关闭「向已释放的 store 添加对象」的告警(在断言释放路径本身的测试里有
* 用)。
*/
static DISABLE_DISPOSED_WARNING: boolean;
constructor();
/**
* @en Dispose of every registered disposable and mark this store as disposed. Later additions are
* dropped with a warning (they are *not* disposed of — see the class docs).
* @zh 释放所有已登记的对象并把本 store 标记为已释放。之后添加进来的对象会被告警丢弃(**不会**
* 被释放,见类文档)。
*/
dispose(): void;
/**
* @en Whether this store has been disposed of.
* @zh 本 store 是否已被释放。
*/
get isDisposed(): boolean;
/**
* @en Dispose of everything currently registered, but keep the store usable.
* @zh 释放当前登记的全部对象,但 store 本身仍可继续使用。
*/
clear(): void;
/**
* @en Register a disposable, returning it for chaining.
* @zh 登记一个可释放对象,并把它返回出来以便链式书写。
*/
add(o: T): T;
/**
* @en Remove a disposable from the store **and** dispose of it. Never throws when it was not part
* of the store.
* @zh 从 store 中移除一个对象**并**释放它。对象本就不在 store 里时也不抛错。
*/
delete(o: T): void;
/**
* @en Remove a disposable from the store **without** disposing of it, handing ownership back to
* the caller.
* @zh 把对象从 store 中移除但**不**释放,所有权交还给调用方。
*/
deleteAndLeak(o: T): void;
/**
* @en Report through the console when this store has already been disposed of, meant for
* assertions in code that must not run after teardown.
* @zh 本 store 已被释放时在控制台报告,用于「释放后不该再执行」的断言。
*/
assertNotDisposed(): void;
}
/**
* @en A map that owns the disposables it stores: overwriting or deleting a key releases the value
* that was there.
*
* @example
* ```ts
* const listeners = new DisposableMap()
* things.forEach(thing => listeners.set(thing, thing.onData(handle)))
* listeners.deleteAndDispose(goneThing) // that one subscription only
* ```
*
* @zh 一个「拥有」其值的 Map:覆盖或删除某个 key 会释放原来挂在那里的对象。
*/
declare class DisposableMap implements CompatDisposable {
#private;
constructor(store?: Map);
/**
* @en Dispose every stored value and mark the map as disposed.
* @zh 释放所有值并把本 map 标记为已释放。
*/
dispose(): void;
/**
* @en Dispose every stored value and empty the map, but keep the map usable.
* @zh 释放所有值并清空 map,但 map 本身仍可继续使用。
*/
clearAndDisposeAll(): void;
/**
* @en Whether a value is stored under `key`.
* @zh `key` 下是否存有值。
*/
has(key: K): boolean;
/**
* @en Number of stored values.
* @zh 已存值的个数。
*/
get size(): number;
/**
* @en The value stored under `key`, if any.
* @zh `key` 下存的值(如果有)。
*/
get(key: K): V | undefined;
/**
* @en Store `value` under `key`, disposing of whatever was there before. Pass
* `skipDisposeOnOverwrite` when the previous value is still referenced elsewhere.
* @zh 把 `value` 存到 `key` 下,并释放原先挂在那里的值。原来的值还被别处引用时,传
* `skipDisposeOnOverwrite`。
*/
set(key: K, value: V, skipDisposeOnOverwrite?: boolean): void;
/**
* @en Remove the value stored under `key` from this map and dispose of it.
* @zh 把 `key` 下的值移出本 map 并释放。
*/
deleteAndDispose(key: K): void;
/**
* @en Remove the value stored under `key` and **return** it — the caller now owns the disposal.
* @zh 把 `key` 下的值移出并**返回**——释放责任归调用方。
*/
deleteAndLeak(key: K): V | undefined;
/**
* @en The stored keys.
* @zh 已存的 key。
*/
keys(): IterableIterator;
/**
* @en The stored values.
* @zh 已存的值。
*/
values(): IterableIterator;
/**
* @en Iterate the `[key, value]` pairs. Iterating does not transfer ownership.
* @zh 遍历 `[key, value]` 对。遍历不会转移所有权。
*/
[Symbol.iterator](): IterableIterator<[K, V]>;
}
/**
* @en A push-based event system: an `Emitter` fires, any number of `Event` subscribers receive.
*
* The pair is deliberately not reactive state. An `Event` carries a signal, not a value you can
* read at any time; a late subscriber has missed everything that already happened. When you need
* "the current value plus changes" use `createExternalState`, `ValueWithChangeEvent` or
* `useSyncExternalStore` instead. In React the usual shape is
*
* ```tsx
* useEffect(() => {
* const sub = emitter.event(handler)
* return () => sub.dispose()
* }, [emitter])
* ```
*
* ## What is different from the original
*
* The event semantics are ported in full — the single-listener fast path, the sparse listener
* array and its compaction, the delivery queue that makes re-entrant `fire()` behave, leak
* detection, refuse-to-add, profiler and every combinator. What changed is only what the original
* borrowed from the rest of `vs/base`:
*
* 1. **Disposal** comes from {@link ./disposable} instead of `lifecycle.ts`/`IDisposable`: the
* protocol is `dispose()`, plus the real `Symbol.dispose` at runtime. `Disposable.None` is
* {@link noopDisposable}.
* 2. **No base-layer imports.** `LinkedList`, `createSingleCallFunction` and `diffSets` are
* implemented privately below, `StopWatch` is a two-line `performance.now()` measurement inside
* {@link EventProfiling}, and the default listener-error handler is a local
* {@link onUnexpectedError} that reports through `console.error` instead of rethrowing on a
* later turn of the loop — a library has no business turning a listener's exception into an
* uncaught global error.
* 3. **`env.VSCODE_DEV`** became {@link isDevelopment} (`process.env.NODE_ENV`), which bundlers
* replace statically so the buffer-leak warnings disappear from production builds.
* 4. **`fromObservable` / `fromObservableLight`** take a minimal structural observable — `get`,
* `reportChanges`, `addObserver`, `removeObserver`. VS Code's `IObservable` carries a whole
* operator set (`read`, `map`, `keepObserved`, `flatten`, …) that belongs to its observable
* implementation, which this library does not ship; anything exposing the four members above
* (including a VS Code observable) still satisfies the interface.
*
* @zh 推送式事件系统:`Emitter` 触发,任意多个 `Event` 订阅者接收。
*
* 它与响应式状态是两回事,刻意不混。`Event` 传递的是信号而非「随时可读的值」;晚到的订阅者
* 已经错过了此前发生的一切。需要「当前值 + 变更通知」时请用 `createExternalState`、
* `ValueWithChangeEvent` 或 `useSyncExternalStore`。React 里的典型写法见上方代码块。
*
* ## 与原版的差异
*
* 事件语义是完整迁移的——单监听器快路径、稀疏监听器数组及其压缩、让重入 `fire()` 行为正确的
* 投递队列、泄漏检测、拒绝新增监听器、性能剖析,以及每一个组合子都在。变的只是原版从 `vs/base`
* 其它模块借来的东西,共四处,逐条列在上面。
*
* 注意第 1 条:本模块的可释放对象协议是 `dispose()`(运行时附带 `Symbol.dispose`),不是
* `IDisposable` 这个类型名本身,理由见 {@link ./disposable} 的模块头。
*/
declare class LinkedList {
#private;
get size(): number;
isEmpty(): boolean;
clear(): void;
push(element: E): () => void;
shift(): E | undefined;
[Symbol.iterator](): Iterator;
}
/**
* @en Passed as the `delay` of {@link Event.debounce} / {@link Event.throttle} to flush on the next
* microtask instead of on a timer. Cheaper and ordered with the rest of the microtask queue, at the
* cost of not coalescing anything that arrives in a later task.
* @zh 作为 {@link Event.debounce} / {@link Event.throttle} 的 `delay` 传入,表示在下一个微任务
* 而不是定时器上冲刷。更便宜,且与其余微任务保持顺序;代价是跨任务到达的事件不会被合并。
*/
declare const MicrotaskDelay: unique symbol;
/**
* @en A promise that can be given up on. `cancel()` detaches the listener but — matching the
* original — does not reject the promise: a cancelled wait simply never settles.
* @zh 一个可以放弃等待的 promise。`cancel()` 会摘掉监听器,但与原版一致,**不会**让 promise
* 变成 rejected:被取消的等待只是永远不结算。
*/
interface CancelablePromise extends Promise {
cancel(): void;
}
/**
* @en A flag that only ever turns one way, plus an event for the turn. Only the read-only face is
* declared here: {@link AsyncEmitter} consumes tokens, it never creates them. Any object with these
* two members works, including VS Code's or `AbortSignal`-based adapters.
* @zh 一个只会单向翻转的标志,外加翻转时的事件。这里只声明只读的一面:{@link AsyncEmitter} 消费
* token,不产生 token。任何具备这两个成员的对象都可以(包括 VS Code 的,或基于 `AbortSignal`
* 的适配器)。
*/
interface CancellationToken {
/**
* @en Whether cancellation has already been requested.
* @zh 是否已经请求了取消。
*/
readonly isCancellationRequested: boolean;
/**
* @en Fires when cancellation is requested. Late subscribers are still called, and only once.
* @zh 取消被请求时触发。晚到的订阅者同样会被调用,且只调用一次。
*/
readonly onCancellationRequested: Event;
}
/**
* @en An observable value, as much of VS Code's interface as {@link Event.fromObservable} needs.
* @zh 可观察值,取 VS Code 接口中 {@link Event.fromObservable} 需要的那部分。
*/
interface IObservable extends IObservableWithChange {
}
/**
* @en An observable value whose changes carry a payload. This library does not ship an observable
* implementation; the interface is here so any compatible one can be adapted to an `Event`.
* @zh 变更带载荷的可观察值。本库不提供 observable 实现,声明这个接口只是为了让任何兼容实现都能
* 被适配成 `Event`。
*/
interface IObservableWithChange {
/**
* @en The current value.
* @zh 当前值。
*/
get(): T;
/**
* @en Force a check for changes and report them to observers. Must not be called from
* {@link IObserver.handleChange}.
* @zh 强制检查变更并上报给观察者。不可在 {@link IObserver.handleChange} 中调用。
*/
reportChanges(): void;
/**
* @en Subscribe an observer (idempotent).
* @zh 订阅一个观察者(幂等)。
*/
addObserver(observer: IObserver): void;
/**
* @en Unsubscribe an observer (idempotent).
* @zh 退订一个观察者(幂等)。
*/
removeObserver(observer: IObserver): void;
}
/**
* @en The receiving half of {@link IObservable}.
* @zh {@link IObservable} 的接收端。
*/
interface IObserver {
/**
* @en A transaction that may have modified `observable` started. Every call is paired with an
* {@link IObserver.endUpdate}.
* @zh 一次可能改动了 `observable` 的事务开始。每次调用都会配对一个
* {@link IObserver.endUpdate}。
*/
beginUpdate(observable: IObservable): void;
/**
* @en That transaction ended — the place to react to it.
* @zh 事务结束——反应变更的地方。
*/
endUpdate(observable: IObservable): void;
/**
* @en `observable` might have changed. Handle lazily or in {@link IObserver.endUpdate}.
* @zh `observable` 可能变了。请惰性处理,或留到 {@link IObserver.endUpdate}。
*/
handlePossibleChange(observable: IObservable): void;
/**
* @en `observable` changed, with the change payload.
* @zh `observable` 发生了变化,并附带变更载荷。
*/
handleChange(observable: IObservableWithChange, change: TChange): void;
}
/**
* @en An event with zero or one parameter that can be subscribed to. The event *is* a function:
* call it with a listener to subscribe, and keep the returned {@link CompatDisposable} to
* unsubscribe. Events are free to be hot (fire before you subscribe), and calling one is cheap —
* the emitter does nothing at all until the first listener arrives.
*
* @example
* ```ts
* class Document {
* private readonly _onDidChange = new Emitter()
* readonly onDidChange: Event = this._onDidChange.event
*
* private edit(text: string) {
* this._onDidChange.fire(text)
* }
* }
*
* const subscription = doc.onDidChange(text => console.log(text))
* subscription.dispose()
* ```
*
* @zh 零参或单参、可被订阅的事件。事件本身就是函数:传入监听器即订阅,保留返回的
* {@link CompatDisposable} 即可退订。事件可以是热的(订阅前就触发过),调用本身很便宜——第一个
* 监听器到来之前 emitter 什么都不做。
*/
type Event = (listener: (e: T) => unknown, thisArgs?: any, disposables?: CompatDisposable[] | DisposableStore) => CompatDisposable;
declare namespace Event {
/**
* @en An event that never fires. Safe to hand out as a default.
* @zh 永不触发的事件。适合作为默认值分发出去。
*/
const None: Event;
/**
* @en Given an event, returns another event which debounces calls and defers the listeners to a
* later task via a shared `setTimeout`. The event is converted into a signal (`Event`) to
* avoid additional object creation as a result of merging events and to try prevent race
* conditions that could arise when using related deferred and non-deferred events.
*
* This is useful for deferring non-critical work (eg. general UI updates) to ensure it does not
* block critical work (eg. latency of keypress to text rendered).
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties", e.g the event is a public
* property. Otherwise a leaked listener on the returned event causes this utility to leak a
* listener on the original event.
*
* @param event The event source for the new event.
* @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed.
* @param disposable A disposable store to add the new EventEmitter to.
*
* @zh 把一个事件变成「合并后延迟到后续任务再通知」的信号事件(统一转成 `Event`,避免合并
* 事件带来的额外对象分配,也尽量避免延迟事件与非延迟事件混用时的竞态)。适合把非关键工作(例如
* 常规 UI 更新)让开,别挡住关键路径(例如按键到出字的延迟)。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到(例如作为公开属性),就必须配一个
* {@link DisposableStore} 使用;否则返回事件上泄漏的监听器会连带在原事件上泄漏一个。
*/
function defer(event: Event, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event;
/**
* @en Given an event, returns another event which only fires once.
*
* @zh 只触发一次的事件。
*/
function once(event: Event): Event;
/**
* @en Fires once, and only when `condition` holds.
* @zh 仅当 `condition` 成立时触发一次。
*/
function onceIf(event: Event, condition: (e: T) => boolean): Event;
/**
* @en Maps an event of one type into an event of another type, like `Array.prototype.map`.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 像 `Array.prototype.map` 一样把一种类型的事件映射成另一种。*注意*:返回的 `Event` 只要
* 会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function map(event: Event, map: (i: I) => O, disposable?: DisposableStore): Event;
/**
* @en Runs `each` on every event object before the listener sees it — the place for a side effect
* that must not change the value.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 在监听器收到事件对象之前先跑一遍 `each`——用于不改变值的副作用。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function forEach(event: Event, each: (i: I) => void, disposable?: DisposableStore): Event;
/**
* @en Wraps an event in another event that fires only when some condition is met. The type-guard
* overload narrows the event type.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 只在条件成立时触发的事件;带类型守卫的重载会同时收窄事件类型。*注意*:返回的 `Event`
* 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function filter(event: Event, filter: (e: T | U) => e is T, disposable?: DisposableStore): Event;
function filter(event: Event, filter: (e: T) => boolean, disposable?: DisposableStore): Event;
function filter(event: Event, filter: (e: T | R) => e is R, disposable?: DisposableStore): Event;
/**
* @en Given an event, returns the same event but typed as `Event`.
* @zh 同一个事件,但类型上是 `Event`——只关心「发生了」,不关心载荷。
*/
function signal(event: Event): Event;
/**
* @en Fires whenever any of the given events fires, carrying that event's payload.
* @zh 任一给定事件触发时都触发,并带上该事件的载荷。
*/
function any(...events: Event[]): Event;
function any(...events: Event[]): Event;
/**
* @en Folds every event object into an accumulator, firing the accumulator after each one. With
* `initial` the first fire sees `merge(initial, first)`; without it the first event object is
* passed through as the initial value.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 把每个事件对象折进累加值,每次折叠后触发一次。给了 `initial` 时首次触发即
* `merge(initial, first)`;不给则把第一个事件对象直接当作初始值透出。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function reduce(event: Event, merge: (last: O | undefined, event: I) => O, initial?: O, disposable?: DisposableStore): Event;
/**
* @en Debounces an event and merges everything that arrives inside the window.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @param event The original event to debounce.
* @param merge A function that reduces all events into a single event.
* @param delay The number of milliseconds to debounce, or `MicrotaskDelay`.
* @param leading Whether to fire a leading event without debouncing.
* @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed.
* Without it, some events could go missing if the last listener leaves inside the window.
* @param leakWarningThreshold See {@link EmitterOptions.leakWarningThreshold}.
* @param disposable A disposable store to register the debounce emitter to.
*
* @zh 防抖:把窗口期内到达的事件合并成一次触发。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*
* `flushOnListenerRemove` 为假时,如果最后一个监听器在窗口期内离开,待触发的那批事件会丢失。
*/
function debounce(event: Event, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event;
function debounce(event: Event, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, flushOnListenerRemove?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event;
/**
* @en Debounces an event, firing after some delay (default 0) with an array of everything that
* arrived in the window. Flushes on listener removal by default, so nothing goes missing.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 防抖并把窗口期内的所有事件对象收集成数组后触发(默认延迟 0)。默认在监听器移除时冲刷,
* 因此不会丢事件。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function accumulate(event: Event, delay?: number | typeof MicrotaskDelay, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event;
/**
* @en Throttles an event, ensuring it fires at most once per delay period. Unlike {@link debounce}
* it can fire on both edges: immediately (`leading`) and after the delay with the merged value of
* everything that arrived meanwhile (`trailing`).
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @zh 节流:每个延迟窗口最多触发一次。与 {@link debounce} 不同,它可以在两端都触发——立即
* (`leading`)以及延迟结束后带窗口期内的合并值(`trailing`)。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function throttle(event: Event, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event;
function throttle(event: Event, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event;
/**
* @en Filters an event such that the same value is not emitted twice in a row — the way to
* collapse "the same window got focus" from two sources into one notification.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @example
* ```ts
* // Fire only one time when a single window is opened or focused
* Event.latch(Event.any(onDidOpenWindow, onDidFocusWindow))
* ```
*
* @zh 连续重复的值只透出一次——把两个来源的「同一个窗口被聚焦」合并成一次通知。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*/
function latch(event: Event, equals?: (a: T, b: T) => boolean, disposable?: DisposableStore): Event;
/**
* @en Splits an event whose parameter is a union into one event per member.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @example
* ```ts
* const event = new Emitter().event
* const [numberEvent, undefinedEvent] = Event.split(event, isUndefined)
* ```
*
* @zh 把联合类型的事件拆成每个成员一个事件。*注意*:返回的 `Event` 只要会被「第三方」拿到,
* 就必须配 {@link DisposableStore} 使用。
*/
function split(event: Event, isT: (e: T | U) => e is T, disposable?: DisposableStore): [Event, Event];
/**
* @en Buffers an event until it has a listener attached, then replays what was buffered.
*
* *NOTE* that this function returns an `Event` and it MUST be called with a {@link DisposableStore}
* whenever the returned event is accessible to "third parties".
*
* @param event The event source for the new event.
* @param debugName A name for this buffer, used in leak detection warnings.
* @param flushAfterTimeout Whether to flush the buffer through a `setTimeout` when the first
* listener is added, so several listeners attached in the same turn all receive it.
* @param _buffer Internal: a source event array used for tests.
*
* @example
* ```ts
* // Start accumulating events; when the first listener is attached, flush
* // after a timeout such that multiple listeners attached before the
* // timeout would receive the event
* this.onInstallExtension = Event.buffer(service.onInstallExtension, 'onInstallExtension', true)
* ```
*
* @zh 在有监听器之前先缓冲事件,等第一个监听器到来后再回放。
*
* *注意*:返回的 `Event` 只要会被「第三方」拿到,就必须配 {@link DisposableStore} 使用。
*
* `flushAfterTimeout` 让回放走一次 `setTimeout`,这样同一轮里挂上来的多个监听器都能收到。
*/
function buffer(event: Event, debugName: string, flushAfterTimeout?: boolean, _buffer?: T[], disposable?: DisposableStore): Event;
/**
* @en Wraps the event in an {@link IChainableSythensis}, allowing a more functional programming
* style. The synthesis object is built once per subscription, and the steps run in the order they
* were written; a step that returns `HaltChainable` (what `filter` and `latch` do when they do
* not pass) stops the chain for that event object.
*
* @example
* ```ts
* // Normal
* const onEnterPressNormal = Event.filter(
* Event.map(onKeyPress.event, e => new StandardKeyboardEvent(e)),
* e => e.keyCode === KeyCode.Enter
* )
*
* // Using chain
* const onEnterPressChain = Event.chain(onKeyPress.event, $ => $
* .map(e => new StandardKeyboardEvent(e))
* .filter(e => e.keyCode === KeyCode.Enter)
* )
* ```
*
* @zh 用链式写法组合事件(见上方示例)。每次订阅构建一个合成器,各步按书写顺序执行;某一步返回
* `HaltChainable`(`filter`、`latch` 不通过时就是它)即中断该事件对象的后续处理。
*/
function chain(event: Event, sythensize: ($: IChainableSythensis) => IChainableSythensis): Event;
/**
* @en The chainable face of an event, as produced by {@link Event.chain}. Methods return the same
* synthesis object, so the calls can be chained; the generic parameter tracks the value flowing
* through, which is why `map` changes it and `filter` only narrows it.
* @zh {@link Event.chain} 提供的链式接口。各方法返回同一个合成器对象,因此可以连写;泛型参数
* 记录流经的值,所以 `map` 会改变它而 `filter` 只做收窄。
*/
interface IChainableSythensis {
map(fn: (i: T) => O): IChainableSythensis;
forEach(fn: (i: T) => void): IChainableSythensis;
filter(fn: (e: T) => e is R): IChainableSythensis;
filter(fn: (e: T) => boolean): IChainableSythensis;
reduce(merge: (last: R, event: T) => R, initial: R): IChainableSythensis;
reduce(merge: (last: R | undefined, event: T) => R): IChainableSythensis;
latch(equals?: (a: T, b: T) => boolean): IChainableSythensis;
}
/**
* @en The subset of a Node.js `EventEmitter` that {@link Event.fromNodeEventEmitter} needs.
* @zh {@link Event.fromNodeEventEmitter} 需要的 Node.js `EventEmitter` 子集。
*/
interface NodeEventEmitter {
on(event: string | symbol, listener: Function): unknown;
removeListener(event: string | symbol, listener: Function): unknown;
}
/**
* @en Creates an {@link Event} from a node event emitter. The source is only touched while
* somebody is listening, so an event nobody subscribes to does not keep the emitter warm.
* @zh 从 node 事件发射器创建 {@link Event}。只在有人监听期间才碰源,因此没人订阅时不会白白
* 占着发射器。
*/
function fromNodeEventEmitter(emitter: NodeEventEmitter, eventName: string, map?: (...args: any[]) => T): Event;
/**
* @en The subset of a DOM `EventTarget` that {@link Event.fromDOMEventEmitter} needs.
* @zh {@link Event.fromDOMEventEmitter} 需要的 DOM `EventTarget` 子集。
*/
interface DOMEventEmitter {
addEventListener(event: string | symbol, listener: any, options?: any): void;
removeEventListener(event: string | symbol, listener: any, options?: any): void;
}
/**
* @en Creates an {@link Event} from a DOM event emitter (an `EventTarget`, e.g. `window` or an
* element). Same laziness as {@link fromNodeEventEmitter}.
* @zh 从 DOM 事件发射器(`EventTarget`,例如 `window` 或某个元素)创建 {@link Event}。惰性
* 行为与 {@link fromNodeEventEmitter} 相同。
*/
function fromDOMEventEmitter(emitter: DOMEventEmitter, eventName: string, map?: (...args: any[]) => T): Event;
/**
* @en Creates a promise out of an event, using {@link Event.once}.
*
* `cancel()` detaches the listener. Matching the original, it does *not* reject the promise — the
* promise simply never settles, so `await`ing a cancelled wait hangs forever. Race it against
* something else if that matters.
*
* @zh 用 {@link Event.once} 把事件变成 promise。
*
* `cancel()` 会摘掉监听器,但与原版一致,**不会**让 promise 变成 rejected——它只是永远不结算,
* 因此 `await` 一个被取消的等待会一直挂着。在意这点的话,请用别的东西和它 race。
*/
function toPromise(event: Event, disposables?: CompatDisposable[] | DisposableStore): CancelablePromise;
/**
* @en A convenience function for forwarding an event to another emitter which improves
* readability. This is similar to {@link Relay} but allows instantiating and forwarding on a
* single line, and also allows for multiple source events.
*
* @example
* ```ts
* Event.forward(event, emitter)
* // equivalent to
* event(e => emitter.fire(e))
* // equivalent to
* event(emitter.fire, emitter)
* ```
*
* @zh 把事件转发到另一个 emitter 的便捷写法,可读性比 `event(e => emitter.fire(e))` 好。
* 与 {@link Relay} 类似,但可以一行内完成创建与转发,也支持多个源事件。
*/
function forward(from: Event, to: Emitter): CompatDisposable;
/**
* @en Adds a listener to an event and calls the listener immediately with `initial` as the event
* object — the shape "render the current state, then keep it updated".
*
* @example
* ```ts
* // Initialize the UI and update it when dataChangeEvent fires
* runAndSubscribe(dataChangeEvent, () => this._updateUI())
* ```
*
* @zh 订阅事件并立刻用 `initial` 调一次监听器——「先渲染当前状态,再跟着更新」的形态。
*/
function runAndSubscribe(event: Event, handler: (e: T) => unknown, initial: T): CompatDisposable;
function runAndSubscribe(event: Event, handler: (e: T | undefined) => unknown): CompatDisposable;
/**
* @en Creates an event that fires when the observable changes, reading the new value for each
* notification. The observable is only observed while somebody listens to the event.
* @zh 观察值变化时触发的事件,每次通知都去读一次新值。只有当有人监听该事件期间才会挂上观察者。
*/
function fromObservable(obs: IObservable, store?: DisposableStore): Event;
/**
* @en Same as {@link fromObservable}, but every listener is attached to the observable directly
* (there is no shared emitter in between) and only the fact that something changed is passed on.
* Cheaper when the value itself is not needed, more expensive with many listeners.
* @zh 与 {@link fromObservable} 类似,但每个监听器直接挂到 observable 上(中间没有共享的
* emitter),并且只传递「变了」这个事实。不需要值时更省,监听器很多时更贵。
*/
function fromObservableLight(observable: IObservable): Event;
}
/**
* @en The hooks an {@link Emitter} exposes around listener bookkeeping. All of them are optional,
* and every one is only called on the transition it names — `onWillAddFirstListener` fires for the
* first subscriber, not for the second.
*
* Two of them are load-bearing for the combinators in {@link Event}: `onWillAddFirstListener` is
* how a derived event starts listening to its source, and `onDidRemoveLastListener` is how it stops,
* which is what keeps an unsubscribed derived event from holding the source alive.
*
* @zh {@link Emitter} 在监听器增删前后暴露的钩子。全部可选,且各自只在对应的一次转变时调用——
* `onWillAddFirstListener` 只在第一个订阅者到来时触发,第二个不会。
*
* 其中两个对 {@link Event} 里的组合子至关重要:`onWillAddFirstListener` 是派生事件挂上源的时机,
* `onDidRemoveLastListener` 是它摘下来的时机,后者正是「没人订阅的派生事件不会拖住源」的原因。
*/
interface EmitterOptions {
/**
* @en Called *before* the very first listener is added.
* @zh 第一个监听器加入*之前*调用。
*/
onWillAddFirstListener?: Function;
/**
* @en Called *after* the very first listener is added.
* @zh 第一个监听器加入*之后*调用。
*/
onDidAddFirstListener?: Function;
/**
* @en Called after a listener is added.
* @zh 每次有监听器加入后调用。
*/
onDidAddListener?: Function;
/**
* @en Called *before* a listener is removed.
* @zh 监听器移除*之前*调用。
*/
onWillRemoveListener?: Function;
/**
* @en Called *after* the very last listener is removed.
* @zh 最后一个监听器移除*之后*调用。
*/
onDidRemoveLastListener?: Function;
/**
* @en Called when a listener throws. Defaults to a `console.error` report (the original in VS Code
* rethrows instead); the remaining listeners still run either way.
* @zh 监听器抛错时调用。默认写 `console.error`(VS Code 原版是重新抛出);两种情况其余监听器
* 都会照常执行。
*/
onListenerError?: (e: any) => void;
/**
* @en Number of listeners allowed before a leak is assumed. Defaults to the globally configured
* value — see {@link setGlobalLeakWarningThreshold}.
* @zh 允许的监听器数量上限,超过即认为泄漏。默认取全局配置值,见
* {@link setGlobalLeakWarningThreshold}。
*/
leakWarningThreshold?: number;
/**
* @en Human-readable name for the emitter, included in leak warning messages so a leak can be
* traced back to its owner.
* @zh emitter 的可读名字,会写进泄漏告警,便于定位是谁在泄漏。
*/
leakWarningName?: string;
/**
* @en Pass in a delivery queue, which is useful for ensuring in-order event delivery across
* multiple emitters.
* @zh 传入一个投递队列,用于保证跨多个 emitter 的事件投递顺序。
*/
deliveryQueue?: EventDeliveryQueue;
/**
* @en ONLY enable this during development. Names the emitter in {@link EventProfiling.all}.
* @zh 仅开发期使用。取的名字会出现在 {@link EventProfiling.all} 里。
*/
_profName?: string;
}
/**
* @en Per-emitter timing, enabled by passing `_profName`. Every instance registers itself in
* {@link EventProfiling.all} (a `Set` that holds strong references — clear it when done) so a
* profiling overlay can walk all emitters in the process.
*
* @zh 每个 emitter 的耗时统计,通过 `_profName` 开启。每个实例都会把自己登记到
* {@link EventProfiling.all}(一个持有强引用的 `Set`——用完记得清)里,方便性能面板遍历进程内
* 全部 emitter。
*/
declare class EventProfiling {
#private;
static readonly all: Set;
private static _idPool;
readonly name: string;
listenerCount: number;
invocationCount: number;
elapsedOverall: number;
durations: number[];
constructor(name: string);
start(listenerCount: number): void;
stop(): void;
}
/**
* @en Sets the process-wide leak warning threshold, returning a disposable that restores the
* previous value — handy in tests that want the warnings on for a single case.
*
* @zh 设置进程级的泄漏告警阈值,返回一个可释放对象用于还原旧值——在只想给单个用例打开告警的
* 测试里很方便。
*/
declare function setGlobalLeakWarningThreshold(n: number): CompatDisposable;
/**
* @en A captured call stack, kept as a string until somebody asks to print it (capturing is cheap,
* formatting is not).
* @zh 一次调用栈快照,在被要求打印之前只以字符串形式保留(抓栈便宜,格式化不便宜)。
*/
declare class Stacktrace {
readonly value: string;
static create(): Stacktrace;
private constructor();
print(): void;
}
/**
* @en The error logged when an emitter goes over its configured listener threshold. `kind` is
* `dominated` when a single call site accounts for most of the listeners (a real leak) and
* `popular` when they come from everywhere.
*
* @zh emitter 超过监听器阈值时记录的错误。`kind` 为 `dominated` 表示绝大多数监听器来自同一个
* 调用点(真泄漏),`popular` 表示各处都有。
*/
declare class ListenerLeakError extends Error {
readonly kind: string;
readonly listenerCount: number;
/**
* @en The detailed message including listener count and most frequent stack. Available locally
* for debugging but intentionally not used as the error `message`.
* @zh 含监听器数量与最高频调用栈的详细消息。本地调试可用,但刻意不作为 error 的 `message`。
*/
readonly details: string;
constructor(kind: 'dominated' | 'popular', details: string, stack: string, listenerCount: number, emitterName?: string);
static is(err: unknown): err is ListenerLeakError;
}
/**
* @en The severe variant, logged when an emitter has gone so far past its threshold that it refuses
* to accept new listeners at all (see {@link Emitter.event}).
* @zh 更严重的一种:emitter 远超阈值时连新监听器都拒绝接受(见 {@link Emitter.event})。
*/
declare class ListenerRefusalError extends ListenerLeakError {
constructor(kind: 'dominated' | 'popular', details: string, stack: string, listenerCount: number, emitterName?: string);
}
/**
* @en Wraps a listener so it can be identified inside the listener list. A single listener is the
* most common case for an emitter, so `Emitter` keeps it unwrapped in an array; the container is
* what makes "remove *this* one" possible without comparing function identity.
* @zh 包住监听器,使其在监听器列表里可被识别。emitter 只有一个监听器是最常见的情形,因此
* `Emitter` 会把它单独存放而不用数组;容器的作用是让「移除这一个」不必比较函数身份。
*/
declare class UniqueContainer {
readonly value: T;
stack?: Stacktrace;
id: number;
constructor(value: T);
}
type ListenerContainer = UniqueContainer<(data: T) => void>;
type ListenerOrListeners = (ListenerContainer | undefined)[] | ListenerContainer;
/**
* @en The Emitter can be used to expose an Event to the public to fire it from the insides.
*
* @example
* ```ts
* class Document {
* private readonly _onDidChange = new Emitter()
* readonly onDidChange: Event = this._onDidChange.event
*
* private doIt(value: string) {
* this._onDidChange.fire(value)
* }
* }
* ```
*
* Three implementation notes worth knowing before changing anything here:
*
* 1. **A single listener is stored bare, not in an array.** Most emitters have one subscriber, so
* the array (and the allocation that comes with it) is avoided until a second one arrives. A
* list never downgrades back to a single container even after removals — swapping between the
* two shapes would cost more than the memory it saves.
* 2. **The listener array can be sparse.** Removal writes `undefined` in place and only compacts
* when more than half the slots are holes, so removing listeners is not quadratic.
* 3. **`fire()` goes through a delivery queue** as soon as there is more than one listener, which
* is what makes re-entrant and nested `fire()` calls deliver in order instead of interleaving,
* and what makes removing a listener during delivery safe.
*
* @zh 对外暴露 `Event`、对内触发的事件发射器。
*
* 改动这里之前值得知道的三点实现取舍:
*
* 1. **单个监听器直接存放,不进数组。** 大多数 emitter 只有一个订阅者,所以数组(以及随之而来的
* 分配)等到第二个订阅者出现才产生。列表一旦形成就不会因为移除而退回单个容器——两种形态来回切换
* 的开销大于省下的内存。
* 2. **监听器数组可以是稀疏的。** 移除时原位写 `undefined`,只有当空洞超过一半时才压缩,因此移除
* 监听器不是平方级开销。
* 3. **一旦监听器多于一个,`fire()` 就走投递队列**,这正是重入与嵌套 `fire()` 能按顺序投递而不是
* 交错的原因,也是投递过程中移除监听器安全的原因。
*/
declare class Emitter {
private readonly _options?;
private readonly _leakWarningThreshold?;
private readonly _leakWarningName?;
private readonly _leakWarningErrorHandler?;
private _leakageMon?;
private readonly _perfMon?;
private _disposed?;
private _event?;
/**
* A listener, or list of listeners.
*
* `_listeners` and `_size` use TS `protected` rather than `#private` on purpose: subclasses
* ({@link AsyncEmitter}, {@link PauseableEmitter}, {@link MicrotaskEmitter}) need them, and `#`
* fields are invisible to subclasses.
*/
protected _listeners?: ListenerOrListeners;
/**
* Always to be defined if `_listeners` is an array. It's no longer a true queue, but holds the
* dispatching 'state'. If `fire()` is called on an emitter, any work left in the `_deliveryQueue`
* is finished first.
*/
private _deliveryQueue?;
protected _size: number;
constructor(options?: EmitterOptions);
private _getLeakageMonitor;
/**
* @en Detach every listener and make the emitter reuse-proof: subscribing to a disposed emitter
* returns {@link noopDisposable} instead of registering anything. Disposal is idempotent.
*
* Remaining listeners are *not* blamed right away — the popular
*
* ```ts
* store.add(model) // (1) create and register the model
* store.add(model.onChange(…)) // (2) subscribe and register the subscription
* store.dispose() // disposes (1) then (2)
* ```
*
* pattern would otherwise warn on every teardown.
*
* @zh 摘掉所有监听器,并让 emitter 之后不再可用:向已释放的 emitter 订阅会返回
* {@link noopDisposable} 而不是登记监听器。释放是幂等的。
*
* 剩余监听器不会被立刻清算——上方那种「先注册模型、再注册订阅」的常见写法会在每次拆卸时误报。
*/
dispose(): void;
/**
* @en For the public to allow to subscribe to events from this Emitter.
*
* The returned function is cached, so `emitter.event === emitter.event` — worth knowing when an
* effect dependency list has `emitter.event` in it.
*
* @zh 供外部订阅本 emitter 的事件。
*
* 返回的函数是缓存的,因此 `emitter.event === emitter.event`——effect 依赖数组里写
* `emitter.event` 时这一点很重要。
*/
get event(): Event;
private _removeListener;
private _deliver;
/** Delivers items in the queue. Assumes the queue is ready to go. */
private _deliverQueue;
/**
* @en To be kept private to fire an event to subscribers.
*
* Fire order is definition order, and it is stable across re-entrant calls: if a listener fires
* the same emitter again, the nested delivery finishes before the outer loop continues.
*
* @zh 触发事件(对外不开放)。投递顺序即注册顺序,并且在重入时保持稳定:如果某个监听器再次触发
* 同一个 emitter,嵌套的那一轮会先投递完,外层循环才继续。
*/
fire(event: T): void;
/**
* @en Whether anybody is listening right now. Useful to skip building an expensive event object
* nobody will receive.
* @zh 当前是否有人监听。可以在没人接收时省掉构造昂贵的事件对象。
*/
hasListeners(): boolean;
}
/**
* @en A marker interface for an event delivery queue. Pass an instance to
* {@link EmitterOptions.deliveryQueue} to make several emitters share one dispatch order — without
* it, each emitter keeps its own and interleaved fires can be observed out of order.
* @zh 事件投递队列的标记接口。传给 {@link EmitterOptions.deliveryQueue} 可让多个 emitter 共享同一
* 份投递顺序——不共享时各 emitter 各持一份,交错触发在观察者看来会乱序。
*/
interface EventDeliveryQueue {
_isEventDeliveryQueue: true;
}
/**
* @en Creates a delivery queue to share between emitters.
* @zh 创建一个可在多个 emitter 之间共享的投递队列。
*/
declare const createEventDeliveryQueue: () => EventDeliveryQueue;
/**
* @en The contract {@link AsyncEmitter} requires of the event object it hands to listeners: a
* cancellation token, plus a way to keep the delivery waiting for asynchronous work.
* @zh {@link AsyncEmitter} 交给监听器的事件对象所需满足的约定:一个取消令牌,以及一种让投递等待
* 异步工作的方式。
*/
interface IWaitUntil {
token: CancellationToken;
waitUntil(thenable: Promise): void;
}
/**
* @en The data half of an {@link IWaitUntil} event object — what the caller passes to
* {@link AsyncEmitter.fireAsync}, with `token` and `waitUntil` added per listener.
* @zh {@link IWaitUntil} 事件对象的数据部分——调用方传给 {@link AsyncEmitter.fireAsync} 的内容,
* `token` 与 `waitUntil` 由框架按监听器补上。
*/
type IWaitUntilData = Omit, 'token'>;
/**
* @en An emitter whose `fireAsync` awaits each listener in turn, in registration order, and lets a
* listener extend its own turn with `waitUntil(promise)`.
*
* This is the "listener can veto / must finish before the next one runs" shape used for things like
* save-participants: every participant gets a chance to contribute, and delivery only moves on once
* they are done. Failures are reported through `onUnexpectedError` and do not stop the others.
*
* @zh 一个「依次等待每个监听器」的 emitter:按注册顺序逐个 await,监听器可以用
* `waitUntil(promise)` 延长自己这一轮。
*
* 这是「监听器可以拦一下 / 必须处理完才轮到下一个」的形态,例如保存参与者:每个参与者都有机会
* 参与,全部处理完才继续往下。失败通过 `onUnexpectedError` 上报,不影响其余监听器。
*/
declare class AsyncEmitter extends Emitter {
private _asyncDeliveryQueue?;
/**
* @en Deliver `data` to every current listener in order, awaiting each one and any promise it
* registered through `waitUntil` before moving on. Stops early once `token` is cancelled; the
* listeners already reached have run.
*
* @param data The event object, without `token`/`waitUntil` (added per listener).
* @param token Cancellation for the whole delivery.
* @param promiseJoin Lets the caller wrap each awaited promise, e.g. to add a timeout or to
* attribute it to the listener for progress reporting.
*
* @zh 按顺序把 `data` 投递给当前每个监听器,逐个等待,并且等它通过 `waitUntil` 注册的 promise
* 都结算后才继续。`token` 被取消时提前结束;已经轮到的监听器已经执行过。
*/
fireAsync(data: IWaitUntilData, token: CancellationToken, promiseJoin?: (p: Promise, listener: Function) => Promise): Promise;
}
/**
* @en An emitter whose events can be held back and replayed later. Useful when changes arrive
* during a batch of work and the subscribers should see them only once the batch is done.
*
* Nesting is counted: every `pause()` needs its own `resume()`, and only the last one flushes.
* While paused nothing is delivered, so a subscriber attached *during* the pause sees nothing until
* the flush — which is the point.
*
* @zh 可以先把事件按住、之后再回放的 emitter。适合「一批工作期间到达的变更,希望订阅者等这批做完
* 才看到」的场景。
*
* 暂停是计数的:每次 `pause()` 都要配一次 `resume()`,只有最后一次会冲刷。暂停期间不投递任何
* 事件,因此在暂停**期间**挂上的订阅者在冲刷前什么都看不到——这正是它的用途。
*/
declare class PauseableEmitter extends Emitter {
private _isPaused;
/** @zh 暂停期间积压的事件。`@en` Events accumulated while paused. */
protected _eventQueue: LinkedList;
private _mergeFn?;
/**
* @en Whether the emitter is currently holding events back.
* @zh 当前是否处于「按住事件」的状态。
*/
get isPaused(): boolean;
constructor(options?: EmitterOptions & {
merge?: (input: T[]) => T;
});
/**
* @en Hold back further events. Counted, so nested pauses need matching resumes.
* @zh 按住后续事件。计数式,因此嵌套暂停需要一一对应地恢复。
*/
pause(): void;
/**
* @en Release the most recent pause; when the count reaches zero, deliver what accumulated — as
* one merged event if `merge` was given, otherwise one by one (and a listener that pauses again
* mid-flush stops the loop, leaving the rest queued).
* @zh 解除最近一次暂停;计数归零时投递积压的事件——给了 `merge` 就合并成一个,否则逐个投递
* (某个监听器在冲刷过程中再次暂停会中止循环,剩余事件留在队列里)。
*/
resume(): void;
fire(event: T): void;
}
/**
* @en Like {@link PauseableEmitter}, but the pause starts by itself on the first `fire()` and lifts
* after `delay` — so a burst collapses into one merged event.
* @zh 与 {@link PauseableEmitter} 类似,但暂停由第一次 `fire()` 自动开始、`delay` 后自动解除——
* 一串密集触发因此塌缩成一次合并事件。
*/
declare class DebounceEmitter extends PauseableEmitter {
private readonly _delay;
private _handle;
constructor(options: EmitterOptions & {
merge: (input: T[]) => T;
delay?: number;
});
fire(event: T): void;
}
/**
* @en An emitter which queues all events and processes them at the end of the current task.
*
* Note the difference from {@link PauseableEmitter}: nobody has to resume this one, and a fire with
* no listeners is dropped (there is no point queueing for nobody).
*
* @zh 把事件排队、在本轮任务末尾统一处理的 emitter。
*
* 与 {@link PauseableEmitter} 的区别:这里不需要谁来恢复;而且没有监听器时直接丢弃(没人为之排队)。
*/
declare class MicrotaskEmitter extends Emitter {
private _queuedEvents;
private _mergeFn?;
constructor(options?: EmitterOptions & {
merge?: (input: T[]) => T;
});
fire(event: T): void;
}
/**
* @en An event emitter that multiplexes many events into a single event.
*
* @example
* ```ts
* // Listen to the `onData` event of all `Thing`s, dynamically adding and removing `Thing`s
* // to the multiplexer as needed.
* const anythingDataMultiplexer = new EventMultiplexer<{ data: string }>()
* const thingListeners = new DisposableMap()
*
* thingService.onDidAddThing(thing => {
* thingListeners.set(thing, anythingDataMultiplexer.add(thing.onData))
* })
* thingService.onDidRemoveThing(thing => {
* thingListeners.deleteAndDispose(thing)
* })
*
* anythingDataMultiplexer.event(e => {
* console.log('Something fired data ' + e.data)
* })
* ```
*
* @zh 把多个事件汇聚成一个事件。
*
* 关键语义:源事件只在**有人订阅聚合事件**期间才被挂上(`add()` 在无人订阅时只是登记,不挂钩),
* 最后一个订阅者离开时全部摘下。
*/
declare class EventMultiplexer implements CompatDisposable {
private readonly emitter;
private hasListeners;
private events;
constructor();
/**
* @en The aggregated event.
* @zh 聚合后的事件。
*/
get event(): Event;
/**
* @en Add a source event, returning a handle that removes it again. Disposing the handle twice is
* safe; removing a source while nobody listens just unregisters it.
* @zh 加入一个源事件,返回用于移除它的句柄。重复释放该句柄是安全的;无人订阅时移除源只是取消
* 登记。
*/
add(event: Event): CompatDisposable;
private onFirstListenerAdd;
private onLastListenerRemove;
private hook;
private unhook;
/**
* @en Unhook every source and dispose the aggregated emitter. The multiplexer cannot be reused
* afterwards.
* @zh 摘掉所有源并释放聚合 emitter。之后不能再用。
*/
dispose(): void;
}
/**
* @en The public face of {@link DynamicListEventMultiplexer}: an aggregated event plus disposal.
* @zh {@link DynamicListEventMultiplexer} 的公开形态:聚合事件加释放能力。
*/
interface IDynamicListEventMultiplexer extends CompatDisposable {
readonly event: Event;
}
/**
* @en Aggregates one event per item of a list that changes over time: items already present are
* hooked immediately, and the `onAddItem` / `onRemoveItem` events keep it in sync as the list
* changes. Each item's subscription is owned by a {@link DisposableMap}, so removing an item
* unhooks exactly that item.
*
* @zh 汇聚「随时间变化的列表」中每项的一个事件:已有项立刻挂钩,之后由 `onAddItem` /
* `onRemoveItem` 保持同步。每项的订阅由 {@link DisposableMap} 持有,因此移除某项只摘掉那一项。
*/
declare class DynamicListEventMultiplexer implements IDynamicListEventMultiplexer {
private readonly _store;
readonly event: Event;
constructor(items: TItem[], onAddItem: Event, onRemoveItem: Event, getEvent: (item: TItem) => Event);
/**
* @en Unhook everything: the item listeners, the two list events and the multiplexer.
* @zh 摘掉所有东西:各项的监听、两个列表事件,以及 multiplexer 本身。
*/
dispose(): void;
}
/**
* @en The EventBufferer is useful in situations in which you want to delay firing your events
* during some code. You can wrap that code and be sure that the event will not be fired during that
* wrap.
*
* ```
* const emitter: Emitter
* const delayer = new EventBufferer()
* const delayedEvent = delayer.wrapEvent(emitter.event)
*
* delayedEvent(console.log)
*
* delayer.bufferEvents(() => {
* emitter.fire() // event will not be fired yet
* })
*
* // event will only be fired at this point
* ```
*
* @zh 用于「某段代码期间先别触发事件」的场景:把那段代码包起来,事件在这段包裹期间不会触发,
* 结束后统一投递(见上方示例)。
*/
declare class EventBufferer {
private data;
/**
* @en Wrap an event so it is buffered while a {@link bufferEvents} call is active.
*
* With `reduce`, all events buffered during the wrap collapse into one call carrying the merged
* value. Without it, every event is replayed one by one afterwards.
*
* **Known upstream limitation of the `reduce` form:** it is only correct with a single listener on
* the returned event. With two or more, each listener pushes the same event into the shared
* accumulator (so `fire(1)` + `fire(2)` with two listeners reduces `1+1+2+2`), and only the first
* listener to arrive gets notified at flush time. This is the original implementation's behaviour,
* kept as-is rather than silently diverging; use the non-reduce form (or `Event.accumulate`) when
* the derived event has several subscribers.
*
* @zh 包一个事件,使其在 {@link bufferEvents} 生效期间被缓冲。
*
* 给了 `reduce` 时,包裹期间缓冲的全部事件会合并成一次投递;不给则结束后逐个回放。
*
* **`reduce` 形式的上游已知限制:** 只有返回的事件「恰好一个监听器」时才是正确的。有两个及以上
* 监听器时,每个监听器都会把同一个事件推进共享累加器(于是两个监听器下 `fire(1)`+`fire(2)` 会
* 累加成 `1+1+2+2`),而且冲刷时只有最先到的那个监听器收到通知。这是原版实现的行为,这里原样
* 保留而不是悄悄改掉;派生事件有多个订阅者时请用非 reduce 形式(或 `Event.accumulate`)。
*/
wrapEvent(event: Event): Event;
wrapEvent(event: Event, reduce: (last: T | undefined, event: T) => T): Event;
wrapEvent(event: Event, reduce: (last: O | undefined, event: T) => O, initial: O): Event;
/**
* @en Run `fn` with buffering on, then flush.
*
* Two behaviours worth knowing, both inherited from the original:
*
* - Nested calls each own a layer, and **every layer flushes its own buffers when it ends** — so
* an event fired inside the inner call is delivered before an event fired before it in the outer
* call. Nesting therefore reverses the order of what each layer buffered; if that matters, do
* not nest.
* - A throw inside `fn` still flushes what was buffered so far (the layer is popped and flushed on
* the way out) and then propagates.
*
* @zh 在开启缓冲的情况下执行 `fn`,随后冲刷。
*
* 两个由原版继承来的行为值得知道:
*
* - 嵌套时每一层各管一层缓冲,**且每一层在结束时都会冲刷自己缓冲的事件**——于是「内层调用里
* 触发的事件」会排在「外层调用里更早触发的事件」前面。嵌套会反转各层缓冲内容的顺序;在意顺序
* 就不要嵌套。
* - `fn` 抛错时已缓冲的内容仍会被冲刷(该层弹栈并冲刷),然后继续向上抛出。
*/
bufferEvents(fn: () => R): R;
}
/**
* @en A Relay is an event forwarder which functions as a replugable event pipe. Once created, you
* can connect an input event to it and it will simply forward events from that input event through
* its own `event` property. The `input` can be changed at any point in time.
*
* @zh 可换源的转发管道。创建后把输入事件接到它上面,它就把该输入事件通过自己的 `event` 转发出去;
* `input` 随时可以更换。
*/
declare class Relay implements CompatDisposable {
private listening;
private inputEvent;
private inputEventListener;
private readonly emitter;
constructor();
/** @zh 转发出去的事件。`@en` The forwarded event. */
readonly event: Event;
/**
* @en Point the relay at a new source. While somebody is listening the old subscription is
* replaced immediately; otherwise the new source is only used the next time the first listener
* arrives.
* @zh 把中继指向新的源。有人监听时立刻换掉旧的订阅;否则新源要等下次第一个监听器到来才被使用。
*/
set input(event: Event);
/**
* @en Detach the input and dispose the internal emitter.
* @zh 摘掉输入并释放内部 emitter。
*/
dispose(): void;
}
/**
* @en A value plus a notification that it changed — the read-anytime counterpart to {@link Event}.
* The event carries no payload; read `value` when you need it.
* @zh 「值 + 变更通知」——{@link Event} 的「随时可读」对应物。事件不带载荷,需要时去读 `value`。
*/
interface IValueWithChangeEvent {
readonly onDidChange: Event;
get value(): T;
}
/**
* @en The mutable, self-contained implementation of {@link IValueWithChangeEvent}: writing `value`
* fires `onDidChange`, but only when the new value differs (`!==`) from the current one.
*
* @example
* ```ts
* const selection = new ValueWithChangeEvent(undefined)
* const sub = selection.onDidChange(() => console.log(selection.value))
* selection.value = 'a' // logs 'a'
* selection.value = 'a' // nothing — same reference
* ```
*
* @zh {@link IValueWithChangeEvent} 的可变实现:写 `value` 会触发 `onDidChange`,但只在 `!==`
* 意义上确实变了时触发。
*/
declare class ValueWithChangeEvent implements IValueWithChangeEvent {
private _value;
/**
* @en A value that never changes, exposing {@link Event.None} as its event — cheaper than a real
* instance when the value is fixed.
* @zh 不变的值,事件就是 {@link Event.None}——值固定时比真造一个实例更省。
*/
static const(value: T): IValueWithChangeEvent;
private readonly _onDidChange;
readonly onDidChange: Event;
constructor(_value: T);
/** @zh 当前值。`@en` The current value. */
get value(): T;
set value(value: T);
}
/**
* @en Keep a {@link DisposableMap} in sync with a set that lives elsewhere: `handleItem` is called
* for each item in the set (only the first time it is seen), and the disposable it returns is
* disposed once the item leaves the set.
*
* Returns a single disposable that ends the tracking — disposing it does *not* dispose the handles
* of items still in the set (they are owned by the returned store the caller may keep).
*
* @param getData Reads the current set.
* @param onDidChangeData Tells when to re-read it.
* @param handleItem Is called for each item in the set (but only the first time the item is seen in
* the set). The returned disposable is disposed if the item is no longer in the set.
*
* @zh 让一个 {@link DisposableMap} 跟随别处维护的集合:集合里每出现一项就调用一次 `handleItem`
* (只在第一次见到该项时调用),该项离开集合时释放它返回的句柄。
*
* 返回一个用于结束追踪的可释放对象——释放它**不会**连带上集合里仍在的那些句柄(它们归调用方持有的
* 那个 store 管)。
*/
declare function trackSetChanges(getData: () => ReadonlySet, onDidChangeData: Event, handleItem: (d: T) => CompatDisposable): CompatDisposable;
/**
* @zh 状态回调函数。对于异步函数,会在状态更新后执行,不会阻塞状态更新,尽可能在外部使用 useEffect 处理异步副作用。
* @en State callback function. Async callbacks run after the state update without blocking it; prefer useEffect for async side effects.
* @template T The type of the state / 状态的类型
* @param newState The new state value / 新的状态值
* @param prevState The previous state value / 之前的状态值
*/
type ExternalStateCallback = (newState: T, prevState: T) => any | Promise;
/**
* @zh 相等性判断函数。默认是 `Object.is`;需要「内容相同即相等」时传入 `shallowEqual`。
* @en Equality predicate. Defaults to `Object.is`; pass `shallowEqual` when equal-by-content
* should count as equal.
* @template S The type of the compared value / 被比较的值的类型
*/
type EqualityFn = (a: S, b: S) => boolean;
/**
* @zh 选择性订阅的回调,第一个参数是当前切片,第二个是上一次的切片(`fireImmediately` 时两者相同)。
* @en Listener for selector subscriptions: the current slice, then the previous slice (both the
* same when `fireImmediately`).
* @template S The type of the selected slice / 被选中的切片的类型
*/
type SelectorListener = (nextSlice: S, prevSlice: S) => void;
/**
* @en Options for `subscribeWithSelector`
* @zh `subscribeWithSelector` 的选项
* @template S The type of the selected slice / 被选中的切片的类型
*/
interface SubscribeSelectorOptions {
/**
* @zh 切片相等性判断,默认 `Object.is`。
* @en Slice equality, `Object.is` by default.
*/
isEqual?: EqualityFn;
/**
* @zh 订阅时立即用当前切片触发一次回调(`nextSlice` 与 `prevSlice` 相同)。
* @en Fire once immediately with the current slice (`nextSlice` and `prevSlice` are the same).
*/
fireImmediately?: boolean;
}
/**
* @en Options for creating external state
* @zh 创建外部状态的选项
* @template T The type of the state / 状态的类型
*/
interface ExternalStateOptions {
/**
* @en Callback invoked on every `set` call, even when the value is unchanged
* @zh 每次调用 `set` 后触发,即使值未发生变化
*/
onSet?: ExternalStateCallback;
/**
* @en Callback invoked only when the stored value actually changes
* @zh 仅在内部存储值发生变化时触发
*/
onChange?: ExternalStateCallback;
/**
* @zh 通知订阅者的时机。
*
* `'sync'`(默认):`set` 返回前就通知完毕,写完立刻读的代码(含测试里的同步断言)都成立。
* `'microtask'`:同一轮任务内多次 `set` 只通知一次,通知在微任务里执行。适合「订阅者多 + 写很频繁」
* 的场景——省下的是每次 `set` 的一遍遍历与通知,React 那侧本来就会合并渲染,所以净语义不变;区别是
* `set` 返回时订阅者还没收到通知,且中间态被跳过(订阅者只看到本轮最后的值)。
* `onSet` / `onChange` 不受影响,仍然逐次同步执行。
* @en When subscribers are notified.
*
* `'sync'` (default): notification completes before `set` returns, so code that reads right after
* writing (including synchronous assertions in tests) holds. `'microtask'`: several `set` calls in
* one task notify once, from a microtask. Worth it when there are many subscribers and writes are
* frequent — what it saves is the per-`set` walk and notification, and since React coalesces
* renders anyway the net semantics are the same; the difference is that subscribers have not been
* notified when `set` returns and intermediate states are skipped (they see the last value of the
* batch). `onSet` / `onChange` are unaffected and still run synchronously for every `set`.
*/
notify?: 'sync' | 'microtask';
}
/**
* @en External state management interface
* @zh 外部状态管理接口
* @template T The type of the state / 状态的类型
*/
interface ExternalState {
/**
* @en Get the current state value
* @zh 获取当前状态值
* @returns The current state value / 当前状态值
*/
get: () => T;
/**
* @zh 设置新的状态值。传入 updater 时必须返回**新引用**:原地修改
* (`set((prev) => {prev.list.push(x); return prev})`)与旧值 `Object.is` 相等,会被判定为
* 「没有变化」,订阅者不会收到通知。
* @en Set a new state value. An updater must return a **new reference**: mutating in place
* (`set((prev) => {prev.list.push(x); return prev})`) compares `Object.is`-equal to the previous
* value, counts as "unchanged", and notifies nobody.
* @param newState The new state value or a function that returns it / 新的状态值或返回新状态的函数
*/
set: (newState: T | ((prevState: T) => T)) => void;
/**
* @en React Hook for using external state in components.
* @zh 在组件中使用外部状态的 React Hook。
* @returns Array containing current state and update function, similar to React useState / 包含当前状态和更新函数的数组,类似于 React useState
*/
useState: () => [T, (newState: T | ((prevState: T) => T)) => void];
/**
* @zh useState 的变体,只获取 value.
* @en A variant of useState that only gets the value.
*/
useGetter: () => T;
/**
* @zh 选择性订阅:只有 `selector` 选出的切片发生变化时才重渲染。
*
* 整份 state 变化时,`set` 依然会通知所有订阅者,但每个消费者的快照是「按 selector 计算 +
* 相等性比较后的缓存切片」;切片相等时 `useSyncExternalStore` 判定快照未变,不调度重渲染。
* 于是改一个字段不会让只读其它字段的组件重渲染。
*
* selector 返回新对象/新数组时(`s => ({a: s.a})`、`s => s.list.filter(...)`)每次都是新引用,
* 默认的 `Object.is` 永远认为「变了」,此时应显式传入 `isEqual`(如 `shallowEqual`)。
* @en Fine-grained subscription: re-render only when the slice picked by `selector` changes.
*
* A `set` still notifies every subscriber, but each consumer's snapshot is the slice computed
* by `selector` and cached with an equality check. When the slice compares equal,
* `useSyncExternalStore` sees an unchanged snapshot and skips the re-render — so writing one
* field no longer re-renders components that read other fields.
*
* A selector that builds a fresh object/array (`s => ({a: s.a})`, `s => s.list.filter(...)`)
* returns a new reference every call, so the default `Object.is` always reports a change; pass
* an explicit `isEqual` (e.g. `shallowEqual`) in that case.
* @param selector Derives the slice from the whole state / 从整份 state 派生切片
* @param isEqual Slice equality, `Object.is` by default / 切片相等性判断,默认 `Object.is`
* @returns The selected slice / 选中的切片
* @example
* ```tsx
* const appState = createExternalState({name: 'wwog', age: 1, theme: 'light'})
*
* // 改 age / theme 都不会让这个组件重渲染
* const name = appState.useSelector((s) => s.name)
*
* // 合成对象必须给相等函数,否则每次都是新引用
* const head = appState.useSelector((s) => ({name: s.name, age: s.age}), shallowEqual)
* ```
*/
useSelector: (selector: (state: T) => S, isEqual?: EqualityFn) => S;
/**
* @zh 在组件外订阅「任意变化」,返回退订函数。组件内请用 `useState` / `useSelector`。
* @en Subscribe to any change outside components; returns an unsubscribe function. Inside
* components use `useState` / `useSelector` instead.
* @param listener Called on every `set` / 每次 `set` 后触发
* @returns Unsubscribe / 退订函数
*/
subscribe: (listener: () => void) => () => void;
/**
* @zh 在组件外按切片订阅:只有 `selector` 选出的切片变化才调用 `listener`,不相关的写入会被跳过。
* 适合「模块级逻辑只关心某几个字段」的场景,也用于替换手写的监听器集合。
* @en Subscribe to a slice outside components: `listener` runs only when the slice changes and
* unrelated writes are skipped. Useful for module-level logic that cares about a few fields, and
* for replacing hand-rolled listener sets.
* @param selector Derives the slice from the whole state / 从整份 state 派生切片
* @param listener Receives (nextSlice, prevSlice); on the first change `prevSlice` is the slice
* as it was when subscribing / 接收 (nextSlice, prevSlice);首次变化时 `prevSlice` 是订阅时的切片
* @param options `isEqual` / `fireImmediately` / `isEqual` 与 `fireImmediately`
* @returns Unsubscribe / 退订函数
* @example
* ```ts
* const stop = appState.subscribeWithSelector(
* (s) => s.age,
* (age, prevAge) => console.log(`age: ${prevAge} → ${age}`),
* )
* appState.set((prev) => ({...prev, theme: 'dark'})) // 切片没变,不触发
* stop()
* ```
*/
subscribeWithSelector: (selector: (state: T) => S, listener: SelectorListener