zeroeffect
Version:
A reactive DOM library with no Signal, no Proxy, no Virtual DOM. Just plain JavaScript objects.
1,993 lines (1,811 loc) • 56.9 kB
text/typescript
type Attributes = Record<string, unknown>;
type Content =
| string
| number
| HTMLElement
| (() => string | number | HTMLElement | null | undefined | false);
type Dependencies = unknown[];
// 标签函数类型 - 支持可变参数
type TagFunction = (
...args: Array<Dependencies | Attributes | Content | Content[]>
) => HTMLElement;
// 常用 HTML 标签,提供更好的类型提示
interface HTagElements {
// 结构元素
div: TagFunction;
p: TagFunction;
span: TagFunction;
section: TagFunction;
article: TagFunction;
header: TagFunction;
footer: TagFunction;
nav: TagFunction;
aside: TagFunction;
main: TagFunction;
// 文本元素
h1: TagFunction;
h2: TagFunction;
h3: TagFunction;
h4: TagFunction;
h5: TagFunction;
h6: TagFunction;
strong: TagFunction;
em: TagFunction;
b: TagFunction;
i: TagFunction;
u: TagFunction;
code: TagFunction;
pre: TagFunction;
blockquote: TagFunction;
// 列表元素
ul: TagFunction;
ol: TagFunction;
li: TagFunction;
dl: TagFunction;
dt: TagFunction;
dd: TagFunction;
// 表单元素
form: TagFunction;
input: TagFunction;
button: TagFunction;
textarea: TagFunction;
select: TagFunction;
option: TagFunction;
label: TagFunction;
fieldset: TagFunction;
legend: TagFunction;
// 表格元素
table: TagFunction;
thead: TagFunction;
tbody: TagFunction;
tfoot: TagFunction;
tr: TagFunction;
th: TagFunction;
td: TagFunction;
caption: TagFunction;
// 媒体元素
img: TagFunction;
video: TagFunction;
audio: TagFunction;
canvas: TagFunction;
svg: TagFunction;
// 链接元素
a: TagFunction;
link: TagFunction;
// 其他常用元素
br: TagFunction;
hr: TagFunction;
meta: TagFunction;
title: TagFunction;
script: TagFunction;
style: TagFunction;
iframe: TagFunction;
embed: TagFunction;
object: TagFunction;
// 支持自定义标签的动态访问索引签名
// 注意:TypeScript 的索引签名允许 undefined,但我们的 Proxy 总是返回 TagFunction
[key: string]: TagFunction | undefined;
}
// 检查值是否应该被渲染(不会渲染的假值)
function shouldRender(value: unknown): boolean {
if (
value === false ||
value === null ||
value === undefined ||
Number.isNaN(value) ||
value === ""
) {
return false;
}
return true;
}
// 将内容渲染到 DOM 节点
function renderContent(content: Content): Node[] {
if (typeof content === "function") {
const result = content();
if (!shouldRender(result)) {
return [];
}
if (result instanceof HTMLElement) {
// 检查是否为列表元素(索引 0)
const listIndex = result.getAttribute("data-ns-list");
if (listIndex === "0") {
insertListRemainingElements(result);
}
return [result];
}
return [document.createTextNode(String(result))];
}
if (!shouldRender(content)) {
return [];
}
if (content instanceof HTMLElement) {
// 检查是否为列表元素(索引 0)
const listIndex = content.getAttribute("data-ns-list");
if (listIndex === "0") {
insertListRemainingElements(content);
}
return [content];
}
return [document.createTextNode(String(content))];
}
// 在第一个元素后插入剩余的列表元素
function insertListRemainingElements(firstElement: HTMLElement): void {
// 使用 WeakMap 查找响应式信息
const info = listFirstElementMap.get(firstElement);
if (!info?.isList || !info.listElements) {
return;
}
const listElements = info.listElements;
const parent = firstElement.parentNode;
// 如果第一个元素尚未在 DOM 中,安排在下一帧插入
if (!parent) {
// 防止重复调度
if (!scheduledInsertions.has(firstElement)) {
scheduledInsertions.add(firstElement);
requestAnimationFrame(() => {
scheduledInsertions.delete(firstElement);
insertListRemainingElements(firstElement);
});
}
return;
}
// 按顺序插入元素
let lastInserted = firstElement;
for (let i = 1; i < listElements.size; i++) {
const element = listElements.get(i);
if (element && !element.parentNode) {
// 在 lastInserted 之后插入
parent.insertBefore(element, lastInserted.nextSibling);
lastInserted = element;
}
}
}
// 直接将样式对象应用到 element.style
function applyStyleObject(
element: HTMLElement,
style: Record<string, string | number>,
): void {
for (const [key, value] of Object.entries(style)) {
// 使用 JavaScript 样式 API 直接设置样式属性
// 使用索引签名动态访问样式属性
(element.style as unknown as Record<string, string>)[key] = String(value);
}
}
// 将响应式属性应用到元素(在初始渲染和更新时调用)
function applyAttributes(
element: HTMLElement,
attrs: Attributes,
skipEvents = false,
): void {
for (const [key, value] of Object.entries(attrs)) {
if (key.startsWith("on") && typeof value === "function") {
// 事件处理器 - 如果 skipEvents 为 true 则跳过(已添加)
if (!skipEvents) {
const eventName = key.slice(2).toLowerCase();
element.addEventListener(eventName, value as EventListener);
}
} else if (typeof value === "function") {
// 响应式属性 - 执行函数并设置结果
const result = value();
if (key === "class" && typeof result === "string") {
element.className = result;
} else if (key === "style") {
if (typeof result === "string") {
element.setAttribute("style", result);
} else if (
result &&
typeof result === "object" &&
!Array.isArray(result) &&
!(result instanceof HTMLElement)
) {
// 样式对象 - 直接应用到 element.style
applyStyleObject(element, result as Record<string, string | number>);
}
} else if (
key === "value" &&
(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement)
) {
// 对于 input/textarea/select 元素,直接设置 value 属性
element.value = String(result ?? "");
} else if (key === "checked" && element instanceof HTMLInputElement) {
// 对于 checkbox/radio 输入框,设置 checked 属性
element.checked = Boolean(result);
} else {
element.setAttribute(key, String(result ?? ""));
}
} else {
// 静态属性
if (key === "class" && typeof value === "string") {
element.className = value;
} else if (key === "style") {
if (typeof value === "string") {
element.setAttribute("style", value);
} else if (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof HTMLElement)
) {
// 样式对象 - 直接应用到 element.style
applyStyleObject(element, value as Record<string, string | number>);
}
} else if (
key === "value" &&
(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement)
) {
// 对于 input/textarea/select 元素,直接设置 value 属性
element.value = String(value ?? "");
} else if (key === "checked" && element instanceof HTMLInputElement) {
// 对于 checkbox/radio 输入框,设置 checked 属性
element.checked = Boolean(value);
} else {
element.setAttribute(key, String(value ?? ""));
}
}
}
}
// 创建响应式元素
function createReactiveElement(
tagName: string,
deps: Dependencies | null,
attrs: Attributes | null,
content: Content | Content[] | null,
): HTMLElement {
const element = document.createElement(tagName);
const container = document.createDocumentFragment();
// 跟踪此元素的依赖项
if (deps) {
for (const dep of deps) {
// 转换为对象作为 Map 键(对象通过引用跟踪)
const depKey = dep as object;
const existing = reactiveElements.get(depKey);
if (existing) {
existing.add({
element,
attrs: attrs || {},
content: content || null,
tagName,
});
} else {
reactiveElements.set(
depKey,
new Set([
{
element,
attrs: attrs || {},
content: content || null,
tagName,
},
]),
);
}
}
}
// 初始渲染 - 应用所有属性包括事件处理器
if (attrs) {
applyAttributes(element, attrs);
}
if (content) {
const contents = Array.isArray(content) ? content : [content];
// 跟踪需要插入的列表首元素
const listFirstElements: HTMLElement[] = [];
for (const item of contents) {
const nodes = renderContent(item);
for (const node of nodes) {
container.appendChild(node);
// 检查是否为列表首元素
if (
node instanceof HTMLElement &&
node.getAttribute("data-ns-list") === "0"
) {
listFirstElements.push(node);
}
}
}
element.appendChild(container);
// 为所有列表首元素插入剩余元素
for (const firstElement of listFirstElements) {
insertListRemainingElements(firstElement);
}
}
return element;
}
// 响应式元素信息
interface ReactiveElementInfo {
element: HTMLElement;
attrs: Attributes;
content: Content | Content[] | null;
tagName: string;
isConditional?: boolean;
condition?: () => unknown; // 返回值不需要是布尔值,只需真值/假值
renderFn?: () => HTMLElement;
elseRenderFn?: () => HTMLElement; // 可选的 else 渲染函数
conditionalPlaceholder?: HTMLElement; // 条件渲染的当前占位符元素
lastConditionValue?: unknown; // 检测变化的最后一个条件值
isList?: boolean;
listData?: unknown[]; // 数组数据(deps 的第一个元素)
listDeps?: unknown[]; // 除列表外的其他依赖项
listRenderFn?: (value: unknown, index: number) => HTMLElement; // 列表项的渲染函数
listElements?: Map<number, HTMLElement>; // 索引到渲染元素的映射
listPlaceholder?: HTMLElement; // 列表渲染的当前占位符元素(首元素或 span)
isVirtualList?: boolean;
virtualListUpdate?: () => void; // 虚拟列表的更新函数
}
// 依赖项到响应式元素的映射
// 使用 WeakMap 进行对象标识跟踪
// 当状态对象被垃圾回收时,WeakMap 自动清理
const reactiveElements = new WeakMap<object, Set<ReactiveElementInfo>>();
// 列表首元素到其响应式信息的映射(用于插入剩余元素)
const listFirstElementMap = new WeakMap<HTMLElement, ReactiveElementInfo>();
// 用于跟踪已安排插入元素的集合(防止重复 requestAnimationFrame)
const scheduledInsertions = new WeakSet<HTMLElement>();
// 更新回调 - 使用 Map 避免内存泄漏(WeakMap 不支持 keys() 方法)
const updateCallbacks = new Map<() => void, boolean>();
// 防止 on 更新回调中无限递归的标志
let isInCallback = false;
// 跟踪元素移除回调的 WeakMap
const removeCallbacks = new WeakMap<HTMLElement, Set<() => void>>();
// 跟踪元素挂载回调的 WeakMap
const mountCallbacks = new WeakMap<
HTMLElement,
{ callback: () => void; hasTriggered: boolean }
>();
// 检测元素移除的全局 MutationObserver
const removalObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const removedNodes = Array.from(mutation.removedNodes);
for (const node of removedNodes) {
// 检查移除的节点是否是具有移除回调的 HTMLElement
if (node instanceof HTMLElement) {
const callbacks = removeCallbacks.get(node);
if (callbacks) {
// 执行此元素的所有回调
for (const callback of callbacks) {
try {
callback();
} catch (error) {
console.error("Error in onRemove callback:", error);
}
}
// 清理
removeCallbacks.delete(node);
}
}
}
}
});
// 检测元素挂载的全局 MutationObserver
const mountObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const addedNodes = Array.from(mutation.addedNodes);
for (const node of addedNodes) {
// 检查添加的节点是否是具有挂载回调的 HTMLElement
if (node instanceof HTMLElement) {
const callbackInfo = mountCallbacks.get(node);
if (callbackInfo && !callbackInfo.hasTriggered) {
// 标记为已触发以防止重复执行
callbackInfo.hasTriggered = true;
try {
callbackInfo.callback();
} catch (error) {
console.error("Error in onMount callback:", error);
}
// 清理 after execution
mountCallbacks.delete(node);
}
}
}
}
});
// DOM 准备后开始观察
if (document.readyState === "loading") {
document.addEventListener(
"DOMContentLoaded",
() => {
removalObserver.observe(document.body, {
childList: true,
subtree: true,
});
mountObserver.observe(document.body, {
childList: true,
subtree: true,
});
},
{ once: true },
);
} else {
removalObserver.observe(document.body, {
childList: true,
subtree: true,
});
mountObserver.observe(document.body, {
childList: true,
subtree: true,
});
}
// 需要更新的状态集合 - 使用 Set(WeakSet 不支持 size/clear/iteration)
const pendingUpdates = new Set<unknown>();
// 跟踪是否已安排批量更新的标志
let updateScheduled = false;
// 处理状态对象的更新
function processUpdate(state: unknown): void {
// 只有对象才能作为 WeakMap 的键
if (typeof state === "object" && state !== null) {
const elements = reactiveElements.get(state as object);
if (!elements) {
return;
}
// 一次处理有效元素的更新并清理过期元素
const staleElements: ReactiveElementInfo[] = [];
for (const info of elements) {
// 检查元素是否仍连接到 DOM
if (!info.element.isConnected) {
staleElements.push(info);
continue;
}
// 处理条件渲染
if (info.isConditional && info.condition && info.renderFn) {
updateConditional(
info,
info.condition,
info.renderFn,
info.elseRenderFn,
);
continue;
}
// 处理虚拟列表渲染
if (info.isVirtualList && info.virtualListUpdate) {
// 如果状态是数组,更新虚拟列表数据引用
const currentState = virtualListInstances.get(info.element);
if (currentState) {
// 如果触发的状态是数组,使用它作为新数据
// 否则保持当前数据
if (Array.isArray(state)) {
currentState.listData = state as unknown[];
}
}
info.virtualListUpdate();
continue;
}
// 处理列表渲染
if (
info.isList &&
info.listData &&
info.listRenderFn &&
info.listElements
) {
// 从触发更新的状态更新列表数据
// 如果状态是列表本身,使用它;否则保持当前列表
const newListData = Array.isArray(state)
? (state as unknown[])
: info.listData;
updateList(info, newListData, info.listRenderFn, info.listElements);
// 更新存储的列表数据
info.listData = newListData;
continue;
}
// 清除现有内容
info.element.textContent = "";
// 重新应用属性(跳过事件处理器,因为已经添加)
applyAttributes(info.element, info.attrs, true);
// 重新渲染内容
if (info.content) {
const container = document.createDocumentFragment();
const contents = Array.isArray(info.content)
? info.content
: [info.content];
for (const item of contents) {
const nodes = renderContent(item);
for (const node of nodes) {
container.appendChild(node);
}
}
info.element.appendChild(container);
}
}
// 清理 stale elements after processing
for (const stale of staleElements) {
elements.delete(stale);
}
}
}
// 批量处理所有待处理的更新
function flushUpdates(): void {
if (pendingUpdates.size === 0) {
updateScheduled = false;
return;
}
// 收集所有要更新的状态
const statesToUpdate: unknown[] = [];
for (const state of pendingUpdates) {
statesToUpdate.push(state);
}
pendingUpdates.clear();
updateScheduled = false;
// 处理所有更新
for (const state of statesToUpdate) {
processUpdate(state);
}
// 触发更新回调
isInCallback = true;
try {
for (const callback of updateCallbacks.keys()) {
callback();
}
} finally {
isInCallback = false;
}
// 如果在回调期间排定了新更新,安排另一次刷新
if (pendingUpdates.size > 0) {
scheduleUpdate();
}
}
// 安排下一帧的批量更新
function scheduleUpdate(): void {
if (updateScheduled) {
return;
}
updateScheduled = true;
// 使用 requestAnimationFrame 以获得更好的性能(在下次重绘前运行)
if (typeof requestAnimationFrame !== "undefined") {
requestAnimationFrame(flushUpdates);
} else {
// 在没有 requestAnimationFrame 的环境中的后备方案
setTimeout(flushUpdates, 0);
}
}
// 更新函数 - 收集要更新的状态并安排下一帧的批量更新
function update(state: unknown): void {
// 将状态添加到待处理更新
pendingUpdates.add(state);
// 如果我们在回调中,还不要安排更新
// 更新将在回调完成后处理(在 flushUpdates 或下次 scheduleUpdate 中)
if (isInCallback) {
return;
}
// 安排下一帧的批量更新
// 这将同一帧中的多个 update() 调用批量处理
scheduleUpdate();
}
// 更新回调 - 支持全局更新和元素绑定更新
// 如果第一个参数是函数,订阅全局更新
// 如果第一个参数是 HTMLElement,订阅更新并自动清理
function onUpdate(callback: () => void): () => void;
function onUpdate(element: HTMLElement, callback: () => void): () => void;
function onUpdate(
param1: (() => void) | HTMLElement,
param2?: () => void,
): () => void {
// 重载 1: onUpdate(callback)
if (typeof param1 === "function") {
updateCallbacks.set(param1, true);
return () => updateCallbacks.delete(param1);
}
// 重载 2: onUpdate(element, callback)
const element = param1 as HTMLElement;
const callback = param2 as () => void;
// 订阅全局更新
updateCallbacks.set(callback, true);
const unsubscribeGlobalUpdate = () => updateCallbacks.delete(callback);
// 元素移除时自动取消订阅
const unsubscribeRemove = onRemove(element, () => {
unsubscribeGlobalUpdate();
});
// 返回一个取消订阅两者的函数
return () => {
unsubscribeGlobalUpdate();
unsubscribeRemove();
};
}
// 监听元素从 DOM 移除
// 返回一个取消订阅函数
function onRemove(element: HTMLElement, callback: () => void): () => void {
const callbacks = removeCallbacks.get(element) || new Set<() => void>();
callbacks.add(callback);
removeCallbacks.set(element, callbacks);
// 返回取消订阅函数
return () => {
const cbs = removeCallbacks.get(element);
if (cbs) {
cbs.delete(callback);
if (cbs.size === 0) {
removeCallbacks.delete(element);
}
}
};
}
// 监听元素挂载/插入 DOM
// 元素挂载时触发一次回调,然后自动移除监听器
// 返回一个取消订阅函数
function onMount(element: HTMLElement, callback: () => void): () => void {
// 存储回调信息
mountCallbacks.set(element, {
callback,
hasTriggered: false,
});
// 如果元素已在 DOM 中,立即触发
if (element.isConnected) {
const callbackInfo = mountCallbacks.get(element);
if (callbackInfo && !callbackInfo.hasTriggered) {
callbackInfo.hasTriggered = true;
try {
callback();
} catch (error) {
console.error("Error in onMount callback:", error);
}
// 清理 immediately
mountCallbacks.delete(element);
}
}
// 返回取消订阅函数
return () => {
mountCallbacks.delete(element);
};
}
// 为给定标签名创建标签函数
function createTagFunction(tagName: string): TagFunction {
return (...args) => {
// 确定传递了哪些参数
let deps: Dependencies | null = null;
let attrs: Attributes | null = null;
let finalContent: Content | Content[] | null = null;
if (args.length === 0) {
// 无参数
return createReactiveElement(tagName, null, null, null);
}
const firstArg = args[0];
const secondArg = args[1];
if (Array.isArray(firstArg) && firstArg.length > 0) {
// 检查第一个数组是依赖项(包含对象)还是内容(包含 HTMLElement/函数/字符串)
const isDependencies = firstArg.every(
(item) =>
typeof item === "object" &&
item !== null &&
!(item instanceof HTMLElement) &&
typeof item !== "function",
);
if (isDependencies) {
// 第一个参数是依赖项数组
deps = firstArg as Dependencies;
if (
secondArg &&
typeof secondArg === "object" &&
!Array.isArray(secondArg) &&
!(secondArg instanceof HTMLElement) &&
typeof secondArg !== "function"
) {
// 第二个参数是属性
attrs = secondArg as Attributes;
// 剩余参数是内容
if (args.length > 2) {
finalContent = args.slice(2) as Content[];
} else {
finalContent = null;
}
} else {
// 第二个参数是内容,剩余参数也是内容
if (args.length > 1) {
finalContent = args.slice(1) as Content[];
} else {
finalContent = null;
}
}
} else {
// 第一个数组是内容(子元素数组)
finalContent = firstArg as Content[];
}
} else if (
firstArg &&
typeof firstArg === "object" &&
!Array.isArray(firstArg) &&
!(firstArg instanceof HTMLElement) &&
typeof firstArg !== "function"
) {
// 第一个参数是属性
attrs = firstArg as Attributes;
// 剩余参数是内容
if (args.length > 1) {
finalContent = args.slice(1) as Content[];
} else {
finalContent = null;
}
} else {
// 第一个参数是内容,所有参数都是内容(多个子元素)
if (args.length === 1) {
finalContent = firstArg as Content | Content[] | null;
} else {
finalContent = args as Content[];
}
}
return createReactiveElement(tagName, deps, attrs, finalContent);
};
}
// CSS 函数 - 创建样式标签并插入 head
function css(styles: string): void {
const styleElement = document.createElement("style");
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
// innerHTML 函数 - 创建 div 元素并设置其 innerHTML
function innerHTML(html: string): HTMLElement {
const div = document.createElement("div");
div.innerHTML = html;
return div;
}
// If 函数 - 基于条件的条件渲染
// 第一个参数必须是数组 (dependencies)
// 第二个参数必须是函数(条件)- 返回真值/假值
// 第三个参数必须是函数(条件为真时渲染)
// 第四个参数是可选函数(条件为假时渲染)
function ifConditional(
deps: Dependencies,
condition: () => unknown,
renderFn: () => HTMLElement,
elseRenderFn?: () => HTMLElement,
): HTMLElement {
const initialElement = document.createElement("span");
initialElement.style.display = "none";
// 跟踪此条件元素的依赖项
const reactiveInfo: ReactiveElementInfo = {
element: initialElement,
attrs: {},
content: null,
tagName: initialElement.tagName.toLowerCase(),
isConditional: true,
condition,
renderFn,
elseRenderFn,
conditionalPlaceholder: initialElement,
};
for (const dep of deps) {
const depKey = dep as object;
const existing = reactiveElements.get(depKey);
if (existing) {
existing.add(reactiveInfo);
} else {
reactiveElements.set(depKey, new Set([reactiveInfo]));
}
}
return initialElement;
}
// 更新条件渲染
function updateConditional(
reactiveInfo: ReactiveElementInfo,
condition: () => unknown,
renderFn: () => HTMLElement,
elseRenderFn?: () => HTMLElement,
): void {
const conditionResult = condition();
const shouldRender = Boolean(conditionResult);
// 检查条件值是否已更改
if (reactiveInfo.lastConditionValue === conditionResult) {
// 条件值未更改,无需重新渲染
return;
}
// 更新最后一个条件值
reactiveInfo.lastConditionValue = conditionResult;
const currentPlaceholder = reactiveInfo.conditionalPlaceholder;
if (!currentPlaceholder) {
// 如果未设置则初始化占位符
const initialPlaceholder = document.createElement("div");
reactiveInfo.conditionalPlaceholder = initialPlaceholder;
reactiveInfo.element = initialPlaceholder;
return;
}
const parent = currentPlaceholder.parentNode;
if (!parent) {
// 占位符尚未在 DOM 中,只更新引用
if (shouldRender) {
const newElement = renderFn();
reactiveInfo.conditionalPlaceholder = newElement;
reactiveInfo.element = newElement;
} else if (elseRenderFn) {
const newElement = elseRenderFn();
reactiveInfo.conditionalPlaceholder = newElement;
reactiveInfo.element = newElement;
} else {
// 创建 span 占位符
const spanPlaceholder = document.createElement("span");
spanPlaceholder.style.display = "none";
reactiveInfo.conditionalPlaceholder = spanPlaceholder;
reactiveInfo.element = spanPlaceholder;
}
return;
}
if (shouldRender) {
// 条件为真 - 用实际元素替换占位符
const newElement = renderFn();
parent.replaceChild(newElement, currentPlaceholder);
// 更新引用 - 新元素成为占位符
reactiveInfo.conditionalPlaceholder = newElement;
reactiveInfo.element = newElement;
} else if (elseRenderFn) {
// 条件为假但有 else 函数 - 用 else 元素替换
const newElement = elseRenderFn();
parent.replaceChild(newElement, currentPlaceholder);
reactiveInfo.conditionalPlaceholder = newElement;
reactiveInfo.element = newElement;
} else {
// 条件为假且没有 else 函数 - 用 span 占位符替换
const spanPlaceholder = document.createElement("span");
spanPlaceholder.style.display = "none";
parent.replaceChild(spanPlaceholder, currentPlaceholder);
reactiveInfo.conditionalPlaceholder = spanPlaceholder;
reactiveInfo.element = spanPlaceholder;
}
}
// 列表函数 - 高效差分渲染列表项
// 第一个参数是数据列表(数组)
// 第二个参数是接收(value, index)的渲染函数
// 类型推断:从 dataList 数组推断 T
function list<T>(
dataList: T[],
renderFn: (value: T, index: number) => HTMLElement,
): HTMLElement;
function list(
dataList: unknown[],
renderFn: (value: unknown, index: number) => HTMLElement,
): HTMLElement;
function list<T = unknown>(
dataList: T[] | unknown[],
renderFn: (value: T, index: number) => HTMLElement,
): HTMLElement {
// 第一个参数必须是数组
if (!Array.isArray(dataList)) {
throw new Error("h.list: 第一个参数必须是数组");
}
const listData = dataList as T[];
const otherDeps: unknown[] = [];
// 转换 renderFn 以匹配内部类型签名
const renderFnInternal = renderFn as (
value: unknown,
index: number,
) => HTMLElement;
// 存储列表元素以进行高效更新
const listElements = new Map<number, HTMLElement>();
// 创建初始占位符 - 将被实际元素替换
let initialPlaceholder: HTMLElement;
if (listData.length === 0) {
// 空列表 - 创建隐藏的 span
initialPlaceholder = document.createElement("span");
initialPlaceholder.style.display = "none";
initialPlaceholder.setAttribute("data-ns-list", "0");
} else {
// 非空列表 - 使用首元素作为占位符
initialPlaceholder = renderFnInternal(listData[0], 0);
initialPlaceholder.setAttribute("data-ns-list", "0");
// 存储首元素
listElements.set(0, initialPlaceholder);
// 渲染剩余元素并存储它们
// 它们将在第一个元素被 renderContent 处理时插入
for (let i = 1; i < listData.length; i++) {
const element = renderFnInternal(listData[i], i);
listElements.set(i, element);
}
}
// 跟踪依赖项(列表和其他 deps)
const allDeps = [listData, ...otherDeps];
const reactiveInfo: ReactiveElementInfo = {
element: initialPlaceholder,
attrs: {},
content: null,
tagName: initialPlaceholder.tagName.toLowerCase(),
isList: true,
listData,
listDeps: otherDeps,
listRenderFn: renderFnInternal,
listElements,
listPlaceholder: initialPlaceholder,
};
// 存储从首元素到响应式信息的映射
if (listData.length > 0) {
listFirstElementMap.set(initialPlaceholder, reactiveInfo);
}
for (const dep of allDeps) {
const depKey = dep as object;
const existing = reactiveElements.get(depKey);
if (existing) {
existing.add(reactiveInfo);
} else {
reactiveElements.set(depKey, new Set([reactiveInfo]));
}
}
return initialPlaceholder;
}
// 更新列表渲染 - 仅管理长度变化以获得最佳性能
// 当长度变化时,重新同步整个列表以确保正确性
function updateList(
reactiveInfo: ReactiveElementInfo,
newListData: unknown[],
renderFn: (value: unknown, index: number) => HTMLElement,
listElements: Map<number, HTMLElement>,
): void {
const currentPlaceholder = reactiveInfo.listPlaceholder;
if (!currentPlaceholder) {
// 如果未设置则初始化
if (newListData.length === 0) {
const spanPlaceholder = document.createElement("span");
spanPlaceholder.style.display = "none";
spanPlaceholder.setAttribute("data-ns-list", "0");
reactiveInfo.listPlaceholder = spanPlaceholder;
reactiveInfo.element = spanPlaceholder;
} else {
const firstElement = renderFn(newListData[0], 0);
firstElement.setAttribute("data-ns-list", "0");
listElements.set(0, firstElement);
// 渲染剩余元素
let lastInserted = firstElement;
for (let i = 1; i < newListData.length; i++) {
const element = renderFn(newListData[i], i);
listElements.set(i, element);
if (firstElement.parentNode) {
firstElement.parentNode.insertBefore(
element,
lastInserted.nextSibling,
);
lastInserted = element;
}
}
reactiveInfo.listPlaceholder = firstElement;
reactiveInfo.element = firstElement;
}
return;
}
const currentLength = listElements.size;
const newLength = newListData.length;
// 仅在长度更改时更新
if (newLength !== currentLength) {
const parent = currentPlaceholder.parentNode;
if (newLength === 0) {
// 列表变为空 - 用 span 占位符替换
const spanPlaceholder = document.createElement("span");
spanPlaceholder.style.display = "none";
spanPlaceholder.setAttribute("data-ns-list", "0");
if (parent) {
// 首先,收集要移除的其他列表元素(不包括 currentPlaceholder)
const elementsToRemove: HTMLElement[] = [];
// 移除所有在 listElements map 中的元素(除 currentPlaceholder)
for (let i = 1; i < currentLength; i++) {
const listElement = listElements.get(i);
if (listElement && listElement.parentNode === parent) {
elementsToRemove.push(listElement);
}
}
// 首先移除其他列表元素
for (const elem of elementsToRemove) {
if (elem.parentNode === parent) {
parent.removeChild(elem);
}
}
// 然后用 span 占位符替换 currentPlaceholder
// 替换前检查 currentPlaceholder 是否仍在 DOM 中
if (currentPlaceholder.parentNode === parent) {
parent.replaceChild(spanPlaceholder, currentPlaceholder);
} else {
// 如果 currentPlaceholder 已被移除,只追加 span
parent.appendChild(spanPlaceholder);
}
}
listElements.clear();
reactiveInfo.listPlaceholder = spanPlaceholder;
reactiveInfo.element = spanPlaceholder;
} else if (currentLength === 0) {
// 列表为空,现在有项目 - 用首元素替换 span
const firstElement = renderFn(newListData[0], 0);
firstElement.setAttribute("data-ns-list", "0");
listElements.set(0, firstElement);
if (parent) {
parent.replaceChild(firstElement, currentPlaceholder);
// 在首元素后插入剩余元素
let lastInserted = firstElement;
for (let i = 1; i < newLength; i++) {
const element = renderFn(newListData[i], i);
listElements.set(i, element);
parent.insertBefore(element, lastInserted.nextSibling);
lastInserted = element;
}
} else {
// 尚未在 DOM 中,只存储引用
for (let i = 1; i < newLength; i++) {
const element = renderFn(newListData[i], i);
listElements.set(i, element);
}
}
reactiveInfo.listPlaceholder = firstElement;
reactiveInfo.element = firstElement;
} else if (newLength > currentLength) {
// 列表长度增加 - 只添加新项目,不重新渲染现有项目
// 找到要插入其后的最后一个现有元素
let lastInserted: HTMLElement | null = null;
if (parent) {
// 找到 DOM 中存在的最后一个列表元素
for (let i = currentLength - 1; i >= 0; i--) {
const listElement = listElements.get(i);
if (listElement && listElement.parentNode === parent) {
lastInserted = listElement;
break;
}
}
}
// 仅渲染和插入新项目(从 currentLength 到 newLength-1)
for (let i = currentLength; i < newLength; i++) {
const element = renderFn(newListData[i], i);
listElements.set(i, element);
if (parent && lastInserted) {
parent.insertBefore(element, lastInserted.nextSibling);
lastInserted = element;
} else if (parent && !lastInserted) {
// 如果没有找到 lastInserted,在 currentPlaceholder 后插入
parent.insertBefore(element, currentPlaceholder.nextSibling);
lastInserted = element;
}
}
} else {
// 列表长度减少 - 重新渲染所有项目(保持现有逻辑)
// 移除元素前保存插入点
// 我们需要找到最后一个列表元素之后的节点,而不是第一个
let insertBeforeNode: Node | null = null;
if (parent && currentPlaceholder.parentNode === parent) {
// 找到最后一个列表元素以获得正确的插入点
let lastListElement: HTMLElement | null = null;
for (let i = currentLength - 1; i >= 0; i--) {
const listElement = listElements.get(i);
if (listElement && listElement.parentNode === parent) {
lastListElement = listElement;
break;
}
}
// 如果我们找到最后一个元素,使用其 nextSibling
// 否则,回退到 currentPlaceholder.nextSibling
if (lastListElement) {
insertBeforeNode = lastListElement.nextSibling;
} else {
insertBeforeNode = currentPlaceholder.nextSibling;
}
}
// 仅移除现有的列表元素(在 listElements map 中)
if (parent) {
const elementsToRemove: HTMLElement[] = [];
// 收集所有需要移除的列表元素
for (let i = 0; i < currentLength; i++) {
const listElement = listElements.get(i);
if (listElement && listElement.parentNode === parent) {
elementsToRemove.push(listElement);
}
}
// 移除所有收集的元素
for (const elem of elementsToRemove) {
if (elem.parentNode === parent) {
parent.removeChild(elem);
}
}
}
listElements.clear();
// 重新渲染所有项目
const firstElement = renderFn(newListData[0], 0);
firstElement.setAttribute("data-ns-list", "0");
listElements.set(0, firstElement);
if (parent) {
// 在原始位置插入,而不是末尾
// 使用前验证 insertBeforeNode 是否仍是父级的子元素
if (insertBeforeNode && insertBeforeNode.parentNode === parent) {
parent.insertBefore(firstElement, insertBeforeNode);
} else if (insertBeforeNode === null) {
// 如果 insertBeforeNode 为空,意味着列表在末尾
parent.appendChild(firstElement);
} else {
// insertBeforeNode 存在但不再是父级的子元素
// 这不应该发生,但回退到 appendChild
parent.appendChild(firstElement);
}
}
// 按顺序插入剩余元素
let lastInserted = firstElement;
for (let i = 1; i < newLength; i++) {
const element = renderFn(newListData[i], i);
listElements.set(i, element);
if (parent) {
parent.insertBefore(element, lastInserted.nextSibling);
lastInserted = element;
}
}
reactiveInfo.listPlaceholder = firstElement;
reactiveInfo.element = firstElement;
}
}
// 如果长度相等,则无需更改 - 单个项目将通过其自己的依赖项更新
}
// ref 函数 - 将响应式属性和依赖项绑定到现有元素
function ref(existingElement: HTMLElement): TagFunction {
return (
...args: Array<Dependencies | Attributes | Content | Content[]>
): HTMLElement => {
// 解析参数(与 createTagFunction 相同)
let deps: Dependencies | null = null;
let attrs: Attributes | null = null;
let content: Content | Content[] | null = null;
for (const arg of args) {
if (Array.isArray(arg) && arg.length > 0) {
// 检查是否为依赖项数组(第一个元素是对象/数组)
const firstItem = arg[0];
if (
typeof firstItem === "object" &&
firstItem !== null &&
!(firstItem instanceof HTMLElement)
) {
deps = arg as Dependencies;
continue;
}
}
if (
typeof arg === "object" &&
arg !== null &&
!(arg instanceof HTMLElement) &&
!Array.isArray(arg)
) {
attrs = arg as Attributes;
continue;
}
if (content === null) {
content = arg as Content | Content[] | null;
} else if (Array.isArray(content)) {
content.push(arg as Content);
} else {
content = [content, arg as Content];
}
}
// 跟踪此元素的依赖项 (same as createReactiveElement)
if (deps) {
for (const dep of deps) {
const depKey = dep as object;
const existing = reactiveElements.get(depKey);
if (existing) {
existing.add({
element: existingElement,
attrs: attrs || {},
content: content || null,
tagName: existingElement.tagName.toLowerCase(),
});
} else {
reactiveElements.set(
depKey,
new Set([
{
element: existingElement,
attrs: attrs || {},
content: content || null,
tagName: existingElement.tagName.toLowerCase(),
},
]),
);
}
}
}
// 应用属性(包括事件处理器)
if (attrs) {
applyAttributes(existingElement, attrs);
}
// 应用内容(替换现有内容)
if (content !== null) {
// 清除现有内容
existingElement.textContent = "";
const container = document.createDocumentFragment();
const contents = Array.isArray(content) ? content : [content];
for (const item of contents) {
const nodes = renderContent(item);
for (const node of nodes) {
container.appendChild(node);
}
}
existingElement.appendChild(container);
}
return existingElement;
};
}
// 虚拟列表类型和函数
type VirtualListOptions = {
/** Item height (fixed number), function to get height, or "auto" for dynamic height measurement */
itemHeight: number | ((index: number) => number) | "auto";
/** 容器高度,默认为父级高度 */
containerHeight?: number;
/** Number of items to render outside viewport (before and after), default 6 */
overscan?: number;
/** 滚动容器元素,如果未提供则使用返回的容器 */
scrollContainer?: HTMLElement;
/** Estimated initial height for dynamic height mode (for first render), default 150 */
estimatedItemHeight?: number;
};
type VirtualListState = {
scrollTop: number;
containerHeight: number;
totalHeight: number;
startIndex: number;
endIndex: number;
visibleItems: HTMLElement[];
spacerTop: HTMLElement | null;
spacerBottom: HTMLElement | null;
listData: unknown[];
renderFn: (value: unknown, index: number) => HTMLElement;
options: VirtualListOptions;
contentContainer: HTMLElement;
rafId: number | null;
itemHeights: Map<number, number>; // 缓存每个项目的实际高度(用于动态高度模式)
estimatedHeight: number; // 估计高度(用于未测量的项目)
};
// 存储虚拟列表实例以进行更新
const virtualListInstances = new WeakMap<HTMLElement, VirtualListState>();
function virtualList<T>(
items: T[],
attrs: Attributes,
renderFn: (value: T, index: number) => HTMLElement,
options?: VirtualListOptions,
): HTMLElement;
function virtualList(
items: unknown[],
attrs: Attributes,
renderFn: (value: unknown, index: number) => HTMLElement,
options?: VirtualListOptions,
): HTMLElement;
function virtualList<T = unknown>(
items: T[] | unknown[],
attrs: Attributes,
renderFn: (value: T, index: number) => HTMLElement,
options?: VirtualListOptions,
): HTMLElement {
// 验证第一个参数必须是数组
if (!Array.isArray(items)) {
throw new Error("h.virtualList: 第一个参数必须是数组");
}
const listData = items as T[];
const otherDeps: unknown[] = [];
const renderFnInternal = renderFn as (
value: unknown,
index: number,
) => HTMLElement;
// 必须传递 attrs 参数,用于设置容器 div 属性
// 配置选项(提供默认值)
const {
itemHeight = "auto",
containerHeight: initialContainerHeight,
overscan = 6,
scrollContainer: externalScrollContainer,
estimatedItemHeight = 150,
} = options || {};
// 确定是否是动态高度模式
const isDynamicHeight = itemHeight === "auto";
// 计算项目高度的函数
const getItemHeight = (index: number, useCache = true): number => {
if (isDynamicHeight) {
// 动态高度模式:优先使用缓存,否则使用估计高度
if (useCache && state.itemHeights.has(index)) {
const cachedHeight = state.itemHeights.get(index);
if (cachedHeight !== undefined) {
return cachedHeight;
}
}
return state.estimatedHeight;
}
if (typeof itemHeight === "function") {
return itemHeight(index);
}
return itemHeight;
};
// 计算总高度
const calculateTotalHeight = (data: unknown[]): number => {
if (isDynamicHeight) {
// 动态高度模式:使用缓存高度 + 估计高度
let total = 0;
for (let i = 0; i < data.length; i++) {
total += getItemHeight(i);
}
return total;
}
if (typeof itemHeight === "function") {
let total = 0;
for (let i = 0; i < data.length; i++) {
total += getItemHeight(i);
}
return total;
}
return data.length * itemHeight;
};
// 计算每个项目的偏移量
const getItemOffset = (index: number, _data: unknown[]): number => {
if (isDynamicHeight || typeof itemHeight === "function") {
let offset = 0;
for (let i = 0; i < index; i++) {
offset += getItemHeight(i);
}
return offset;
}
return index * itemHeight;
};
// 确保选项不为空
const finalOptions = options || {
itemHeight: "auto",
overscan: 6,
estimatedItemHeight: 150,
};
// 状态
const state: VirtualListState = {
scrollTop: 0,
// 初始高度为 0,等待父级容器高度检测
containerHeight: initialContainerHeight || 0,
totalHeight: 0,
startIndex: 0,
endIndex: 0,
visibleItems: [],
spacerTop: null,
spacerBottom: null,
listData: listData as unknown[],
renderFn: renderFnInternal,
options: finalOptions,
contentContainer: null as unknown as HTMLElement,
rafId: null,
itemHeights: new Map<number, number>(),
estimatedHeight: estimatedItemHeight,
};
// 创建容器
const container = document.createElement("div");
container.style.position = "relative";
container.style.overflow = "auto";
// 应用用户提供的 attrs
applyAttributes(container, attrs);
if (initialContainerHeight) {
container.style.height = `${initialContainerHeight}px`;
}
// 创建内容容器
const contentContainer = document.createElement("div");
contentContainer.style.position = "relative";
container.appendChild(contentContainer);
// 计算最大高度限制(屏幕高度的 2 倍)
const maxHeight = typeof window !== "undefined" ? window.innerHeight * 2 : 0;
// 节流器:在滚动期间检测高度
let resizeThrottleId: number | null = null;
const throttledHeightCheck = () => {
if (resizeThrottleId === null) {
resizeThrottleId = requestAnimationFrame(() => {
resizeThrottleId = null;
// 检测容器高度变化
const currentHeight = container.offsetHeight;
if (currentHeight > 0) {
// 限制高度不超过屏幕高度的 2 倍
const newHeight = Math.min(currentHeight, maxHeight);
if (newHeight !== state.containerHeight) {
state.containerHeight = newHeight;
container.style.height = `${newHeight}px`;
// 重新渲染可见项目
renderVisibleItems(state.listData);
}
}
});
}
};
// 高度检测函数:等待容器准备就绪
const checkContainerHeight = (): void => {
// 首先尝试读取容器自己的 offsetHeight
const containerOffsetHeight = container.offsetHeight;
if (initialContainerHeight) {
// 如果设置了初始高度,直接使用
state.containerHeight = initialContainerHeight;
container.style.height = `${initialContainerHeight}px`;
} else if (containerOffsetHeight > 0) {
// 如果容器有明确高度,使用它
const newHeight = Math.min(containerOffsetHeight, maxHeight);
if (newHeight !== state.containerHeight) {
state.containerHeight = newHeight;
container.style.height = `${newHeight}px`;
}
} else {
// 容器没有高度,尝试读取父级容器高度
const parentHeight = container.parentElement?.clientHeight || 0;
if (parentHeight === 0) {
// 父级容器还没有高度,继续等待下一帧
requestAnimationFrame(checkContainerHeight);
return;
}
// 限制高度不超过屏幕高度的 2 倍
const newHeight = Math.min(parentHeight, maxHeight);
if (newHeight !== state.containerHeight) {
state.containerHeight = newHeight;
container.style.height = `${newHeight}px`;
}
}
// 初始渲染
renderVisibleItems(state.listData);
};
// 将内容容器设置到状态
state.contentContainer = contentContainer;
// 计算可见范围
const calculateVisibleRange = (
data: unknown[],
): { start: number; end: number } => {
if (data.length === 0) {
return { start: 0, end: 0 };
}
const scrollTop = state.scrollTop;
const containerHeight = state.containerHeight;
const viewportTop = scrollTop;
const viewportBottom = scrollTop + containerHeight;
let start = 0;
let end = data.length - 1;
// 二进制搜索起始索引
if (isDynamicHeight || typeof itemHeight === "function") {
// 可变高度:使用二进制搜索
let low = 0;
let high = data.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const offset = getItemOffset(mid, data);
const itemHeightValue = getItemHeight(mid);
if (offset + itemHeightValue < viewportTop) {
low = mid + 1;
} else {
high = mid - 1;
}
}
start = low;
// 查找结束索引
low = start;
high = data.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const offset = getItemOffset(mid, data);
if (offset > viewportBottom) {
high = mid - 1;
} else {
low = mid + 1;
}
}
end = high;
} else {
// 固定高度:直接计算
start = Math.floor(viewportTop / itemHeight);
end = Math.min(Math.ceil(viewportBottom / itemHeight), data.length - 1);
}
// 应用overscan
start = Math.max(0, start - overscan);
end = Math.min(data.length - 1, end + overscan);
return { start, end };
};
// 渲染可见项目
const renderVisibleItems = (data: unknown[]): void => {
const { start, end } = calculateVisibleRange(data);
state.startIndex = start;
state.endIndex = end;
// 移除旧的可见项目
for (const item of state.visibleItems) {
if (item.parentNode === contentContainer) {
contentContainer.removeChild(item);
}
}
state.visibleItems = [];
// 创建顶部间距
if (start > 0) {
const topOffset = getItemOffset(start, data);
if (!state.spacerTop) {
state.spacerTop = document.createElement("div");
state.spacerTop.style.position = "absolute";
state.spacerTop.style.top = "0";
state.spacerTop.style.left = "0";
state.spacerTop.style.right = "0";
contentContainer.appendChild(state.spacerTop);
}
state.spacerTop.style.height = `${topOffset}px`;
} else if (state.spacerTop) {
contentContainer.removeChild(state.spacerTop);
state.spacerTop = null;
}
// 渲染可见项目
for (let i = start; i <= end; i++) {
const item = renderFnInternal(data[i], i);
item.style.position = "absolute";
item.style.top = `${getItemOffset(i, data)}px`;
item.style.left = "0";
item.style.right = "0";
contentContainer.appendChild(item);
state.visibleItems.push(item);
}
// 动态 height 模式:测量并缓存实际高度
if (isDynamicHeight && state.visibleItems.length > 0) {
// 使用 requestAnimationFrame 确保 DOM 被渲染
requestAnimationFrame(() => {
let needsUpdate = false;
const currentStart = state.startIndex;
for (let idx = 0; idx < state.visibleItems.length; idx++) {
const item = state.visibleItems[idx];
if (!item) continue;
const actualIndex = currentStart + idx;
const measuredHeight =
item.offsetHeight || item.getBoundingClientRect().height;
if (measuredHeight > 0) {
const oldHeight =
state.itemHeights.get(actualIndex) || state.estimatedHeight;
if (Math.abs(measuredHeight - oldHeight) > 1) {
// 高度已更改,更新缓存
state.itemHeights.set(actualIndex, measuredHeight);
needsUpdate = true;
}
}
}
// 更新估计高度(使用测量项目的平均值)
if (needsUpdate && state.itemHeights.size > 0) {
let sum = 0;
let count = 0;
for (const height of state.itemHeights.values()) {
sum += height;
count++;
}
state.estimatedHeight = Math.round(sum / count);
// 重新计算总高度并更新布局
state.totalHeight = calculateTotalHeight(data);
contentContainer.style.height = `${state.totalHeight}px`;
// 更新所有可见项目的位置
for (let idx = 0; idx < state.visibleItems.length; idx++) {
const item = state.visibleItems[idx];
if (!item) continue;
const actualIndex = currentStart + idx;
item.style.top = `${getItemOffset(actualIndex, data)}px`;
}
// 更新间距
if (currentStart > 0 && state.spacerTop) {
state.spacerTop.style.height = `${getItemOffset(currentStart, data)}px`;
}
const bottomOffset =
getItemOffset(data.length, data) - getItemOffset(end + 1, data);
if (bottomOffset > 0 && state.spacerBottom) {
state.spacerBottom.style.height = `${bottomOffset}px`;
state.spacerBottom.style.top = `${getItemOffset(end + 1, data)}px`;
}
}
});
}
// 创建底部间距
const bottomOffset =
getItemOffset(data.length, data) - getItemOffset(end + 1, data);
if (bottomOffset > 0) {
if (!state.spacerBottom) {
state.spacerBottom = document.createElement("div");
state.spacerBottom.style.position = "absolute";
state.spacerBottom.style.bottom = "0";
state.spacerBottom.style.left = "0";
state.spacerBottom.style.right = "0";
contentContainer.appendChild(state.spacerBottom);
}
state.spacerBottom.style.height = `${bottomOffset}px`;
state.spacerBottom.style.top = `${getItemOffset(end + 1, data)}px`;
} else if (state.spacerBottom) {
contentContainer.removeChild(state.spacerBottom);
state.spacerBottom = null;
}
// 更新内容容器的总高度
state.totalHeight = calculateTotalHeight(data);
contentContainer.style.height = `${state.totalHeight}px`;
};
// 处理滚动
const handleScroll = (): void => {
const scrollContainer = externalScrollContainer || container;
state.scrollTop = scrollContainer.scrollTop;
// 在滚动期间检查高度
throttledHeightCheck();
if (state.rafId === null) {
state.rafId = requestAnimationFrame(() => {
state.rafId = null;
renderVisibleItems(state.listData);
});
}
};
// 更新函数(数据更改时调用)
const updateVirtualList = (newData: unknown[]): void => {
state.listData = newData;
// 动态高度模式:清除不再存在的高度缓存
if (isDynamicHeight) {
const newLength = newData.length;
for (const index of state.itemHeights.keys()) {
if (index >= newLength) {
state.itemHeights.delete(index);
}
}
}
state.totalHeight = calculateTotalHeight(newData);
contentContainer.style.height = `${state.totalHeight}px`;
renderVisibleItems(newData);
};
// 初始化
const init = (): void => {
// 设置总高度
state.totalHeight = calculateTotalHeight(listData as unknown[]);
contentContainer.style.height = `${state.totalHeight}px`;
// 添加滚动监听器
const scrollContainer = externalScrollContainer || container;
scrollContainer.addEventListener("scroll", handleScroll, { passive: true });
// 开始检测容器高度(等待父级准备就绪)
checkContainerHeight();
};
// 初始化
init();
// 存储状态以进行更新
virtualListInstances.set(container, state);
// 注册到响应式系统,数据数组更改时更新
for (const dep of [listData, ...otherDeps]) {
const depKey = dep as object;
const existing = reactiveElements.get(depKey);
if (existing) {
existing.add({
element: container,
attrs: {},
content: null,
tagName: "div",
isVirtualList: true,
virtualListUpdate: () => {
const currentState = virtualListInstances.get(container);
if (currentState) {
updateVirtualList(currentState.listData);
}
},
});
} else {
reactiveElements.set(
depKey,
new Set([
{
element: container,
attrs: {},
content: null,
tagName: "div",
isVirtualList: true,
virtualListUpdate: () => {
const currentState = virtualListInstances.get(container);
if (currentState) {
updateVirtualList(currentState.listData);
}
},
},
]),
);
}
}
return container;
}
// 不应是标签函数的方法白名单
const WHITELIST = new Set([
"update",
"onUpdate",
"onRemove",
"onMount",
"watch",
"list",
"if",
"css",
"innerHTML",
"ref",
"virtualList",
]);
// 动态标签访问的代理处理器
const handler: ProxyHandler<HObject> = {
get(
target,
prop: string,
):
| TagFunction
| typeof update
| typeof onUpdate
| typeof onRemove
| typeof onMount
| typeof css
| typeof innerHTML
| typeof ifConditional
| typeof list
| typeof ref
| typeof virtualList {
if (WHITELIST.has(prop)) {
if (prop === "update") {
return update;
}
if (prop === "onUpdate") {
return onUpdate;
}
if (prop === "onRemove") {
return onRemove;
}
if (prop === "onMount") {
return onMount;
}
if (prop === "css") {
return css;
}
if (prop === "innerHTML") {
return innerHTML;
}
if (prop === "if") {
return ifConditional;
}
if (prop === "list") {
return list;
}
if (prop === "ref") {
return ref;
}
if (prop === "virtualList") {
return virtualList;
}
// 对于 watch - 目前返回无操作函数
return (() => {}) as TagFunction;
}
// 按需创建标签函数
if (!target[prop]) {
target[prop] = createTagFunction(prop);
}
// 始终为标签名返回 TagFunction
return target[prop] as TagFunction;
},
};
// 具有所有方法和标签函数的 H 对象接口
// 使用交叉类型允许特殊方法覆盖索引签名
// Record<string, TagFunction> 确保所有字符串键返回 TagFunction
type HObject = {
// 特殊方法(这些具有特定类型)
update: typeof update;
onUpdate: typeof onUpdate;
onRemove: typeof onRemove;
onMount: typeof onMount;
css: typeof css;
innerHTML: typeof innerHTML;
if: typeof ifConditional;
list: typeof list;
ref: typeof ref;
virtualList: typeof virtualList;
} & HTagElements & {
// 动态标签的索引签名 - 始终通过 Proxy 返回 TagFunction
[key: string]:
| TagFunction
| typeof update
| typeof onUpdate
| typeof onRemove
| typeof onMount
| typeof css
| typeof innerHTML
| typeof ifConditional
| typeof list
| typeof ref
| typeof virtualList;
};
// 使用代理创建 h 对象
const _h = new Proxy<HObject>({} as HObject, handler);
// 处理 noUncheckedIndexedAccess 的导出类型
// 我们需要显式定义类型以覆盖索引签名行为
// 关键是使用没有 noUncheckedIndexedAccess 行为的类型
type HExport = {
// 特殊方法
update: typeof update;
onUpdate: typeof onUpdate;
onRemove: typeof onRemove;
onMount: typeof onMount;
css: typeof css;
innerHTML: typeof innerHTML;
if: typeof ifConditional;
list: typeof list;
ref: typeof ref;
virtualList: typeof virtualList;
} & HTagElements & {
// 明确返回 TagFunction 的索引签名(不是 TagFunction | undefined)
// 这覆盖了来自 noUncheckedIndexedAccess 的默认行为
[key: string]:
| TagFunction
| typeof update
| typeof onUpdate
| typeof onRemove
| typeof onMount
| typeof css
| typeof innerHTML
| typeof ifConditional
| typeof list
| typeof ref
| typeof virtualList;
};
const h = _h as unknown as HExpo