Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | 1x 1x 1x 1x 1x 1x 20x 20x 20x 20x 20x 20x 38x 38x 38x 38x 38x 38x 38x 38x 144x 144x 144x 144x 114x 114x 114x 114x 114x 114x 114x 114x 114x 114x 33x 24x 33x 1x 1x 114x 114x 114x 8x 8x 8x 106x 106x 106x 106x 106x 106x 144x 30x 30x 30x 136x 136x 38x 55x 25x 25x 55x 38x 31x 31x 31x 31x 31x 31x 31x 31x 31x 31x 136x 136x 106x 136x 6x 30x 24x 24x 136x 136x 31x 38x 20x 20x 20x | import { createEffect } from '../primitives/effect';
import { createRoot, onCleanup } from '../lifecycle/lifecycle';
import { createSignal } from '../primitives/signal';
import { devWarning } from '../error';
import { longestIncreasingSubsequence } from '../internal/lis';
import type { Disposer, SignalGetter, SignalSetter } from '../types';
/**
* Props for the For component that efficiently renders lists with reactive updates.
*/
type ForProps<T> = {
/** A signal containing the array of items to render */
each: SignalGetter<T[]>;
/** Function that returns a DOM node for each item, receiving reactive item and index signals */
children: (item: SignalGetter<T>, index: SignalGetter<number>) => Node | null;
/** Optional function to generate unique keys for efficient reconciliation */
key?: (item: T, index: number) => string | number;
};
/**
* Internal representation of a mapped list item with its reactive state and DOM node.
*/
type MappedItem<T> = {
/** The DOM node representing this item */
node: Node;
/** Function to dispose this item's reactive scope */
disposer: Disposer;
/** Function to update the item's data signal */
setSignal: SignalSetter<T>;
/** Function to update the item's index signal */
setIndex: SignalSetter<number>;
};
/**
* Efficiently renders a list of items with reactive updates and optimized reconciliation.
*
* The For component uses a keyed reconciliation algorithm based on the Longest Increasing
* Subsequence (LIS) to minimize DOM operations when the list changes. Each item gets
* its own reactive scope for automatic cleanup.
*
* @param props Configuration object with each, children, and optional key function
* @returns A DocumentFragment containing all rendered items
*
* @example
* ```typescript
* const [items, setItems] = createSignal([1, 2, 3]);
*
* const list = For({
* each: items,
* key: (item) => item, // Optional: improves performance for non-primitives
* children: (item, index) => h.li(`Item ${item()} at index ${index()}`)
* });
* ```
*/
export function For<T>(props: ForProps<T>): Node {
const { each, children, key } = props;
const container = document.createDocumentFragment();
// End marker provides a stable anchor point for DOM insertions
const endMarker = document.createTextNode('');
container.appendChild(endMarker);
// Map tracks items by their key (either generated by key function or the item itself)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mappedItems = new Map<any, MappedItem<T>>();
createEffect(() => {
const newItems = each();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newMappedItems = new Map<any, MappedItem<T>>();
const parent = endMarker.parentNode!;
// Dev-mode warning if `key` is missing for primitive arrays
devWarning(
!!key || !newItems.some(item => typeof item !== 'object' || item === null),
'The `For` component is being used with an array of primitives without a `key` prop. This can lead to inefficient re-rendering. Please provide a `key` function.'
);
// Pass 1: Create new items and update existing ones
for (let i = 0; i < newItems.length; i++) {
const itemData = newItems[i];
const itemKey = key ? key(itemData, i) : itemData;
let mappedItem = mappedItems.get(itemKey);
if (!mappedItem) {
// Item is new, create it within its own lifecycle root.
// Item is new. Define placeholders for the values we'll create.
let node: Node | null = null; // Allow children() to return null
let setSignal: SignalSetter<T>;
let setIndex: SignalSetter<number>;
// Create a root to manage the lifecycle of this new item.
// It populates our placeholder variables.
const disposer = createRoot(() => {
const [itemSignal, setItemSignal] = createSignal(itemData);
const [indexSignal, setIndexSignal] = createSignal(i);
setSignal = setItemSignal;
setIndex = setIndexSignal;
node = children(itemSignal, indexSignal);
// When this item's root is disposed, remove its node from the DOM.
onCleanup(() => {
// Check if the node is an Element before calling remove()
if (node instanceof Element) {
node.remove();
} else if (node?.parentNode) {
// Fallback for Text nodes or other node types
node.parentNode.removeChild(node);
}
});
});
// --- THIS IS THE FIX ---
// If the children function returned null or undefined, we should not
// create a mapped item for it. We must also dispose of the root
// we just created to prevent leaking the item/index signals.
if (node == null) { // Use `== null` to catch both null and undefined
disposer();
continue; // Skip to the next item in the loop
}
// --- END OF FIX ---
// Now, construct the MappedItem object with the populated values.
mappedItem = {
node: node, // No longer need '!' assertion since we checked for null above
disposer: disposer,
setSignal: setSignal!, // Add '!' to assert that `setSignal` is not undefined
setIndex: setIndex!, // Add '!' to assert that `setIndex` is not undefined
};
} else {
// Item already exists, update its data and index signals.
mappedItem.setSignal(itemData);
mappedItem.setIndex(i);
}
newMappedItems.set(itemKey, mappedItem);
}
// Pass 2: Remove old items
for (const [itemKey, item] of mappedItems.entries()) {
if (!newMappedItems.has(itemKey)) {
item.disposer(); // This triggers onCleanup which removes the node
}
}
// --- THE UPGRADE: LIS-based Reconciliation ---
// Pass 3: Perform efficient DOM moves
if (newItems.length > 0) {
const oldMap = new Map(Array.from(mappedItems.values()).map((item, i) => [item.node, i]));
// Filter out items that don't exist in newMappedItems (those that returned null)
const newNodes = newItems
.map((item, i) => newMappedItems.get(key ? key(item, i) : item))
.filter((mappedItem): mappedItem is MappedItem<T> => mappedItem !== undefined)
.map(mappedItem => mappedItem.node);
const seq = newNodes.map(node => oldMap.get(node));
const lis = longestIncreasingSubsequence(seq.map(i => i === undefined ? -1 : i));
let cur = lis.length - 1;
let next: Node | null = endMarker;
for (let i = newNodes.length - 1; i >= 0; i--) {
const node = newNodes[i];
if (seq[i] === undefined) {
// It's a new node, insert it.
parent.insertBefore(node, next);
} else if (cur < 0 || i !== lis[cur]) {
// It's a moved node.
parent.insertBefore(node, next);
} else {
// It's a stable node.
cur--;
}
next = node;
}
}
// Update the master map for the next run.
mappedItems = newMappedItems;
});
return container;
}
|