Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions packages/core/src/strategies/layout/FixedLayoutStrategy.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import type { Range } from '../../types';
import type { LayoutStrategy } from './LayoutStrategy';
import { clamp } from '../../utils/clamp';
import type { Range } from "../../types";
import type { LayoutStrategy } from "./LayoutStrategy";
import { clamp } from "../../utils/clamp";

export class FixedLayoutStrategy implements LayoutStrategy {
constructor(private itemSize: number) {}
readonly #itemSize: number;

constructor(itemSize: number) {
this.#itemSize = itemSize;
}

getItemOffset(index: number): number {
return index * this.itemSize;
return index * this.#itemSize;
}

getItemSize(_index: number): number {
return this.itemSize;
return this.#itemSize;
}

getTotalSize(count: number): number {
return count * this.itemSize;
return count * this.#itemSize;
}

getVisibleRange(
scrollOffset: number,
viewportSize: number,
count: number
): Range {
const startIndex = clamp(
0,
Math.floor(scrollOffset / this.itemSize),
count - 1
);
const startIndex = clamp(0, (scrollOffset / this.#itemSize) | 0, count - 1);

const visibleCount = Math.ceil(viewportSize / this.itemSize);
const visibleCount = Math.ceil(viewportSize / this.#itemSize);
const endIndex = Math.min(count - 1, startIndex + visibleCount);

return { startIndex, endIndex };
}
}

30 changes: 15 additions & 15 deletions packages/core/src/strategies/scroll/VirtualScrollSource.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import type { ScrollSource } from "./ScrollSource";

export class VirtualScrollSource implements ScrollSource {
private scrollOffset = 0;
private viewportSize = 0;
private listeners = new Set<(offset: number) => void>();
#scrollOffset = 0;
#viewportSize = 0;
#listeners = new Set<(offset: number) => void>();

getScrollOffset(): number {
return this.scrollOffset;
return this.#scrollOffset;
}

getViewportSize(): number {
return this.viewportSize;
return this.#viewportSize;
}

setScrollOffset(offset: number): void {
if (this.scrollOffset !== offset) {
this.scrollOffset = offset;
this.notifyListeners();
if (this.#scrollOffset !== offset) {
this.#scrollOffset = offset;
this.#notifyListeners();
}
}

setViewportSize(size: number): void {
if (this.viewportSize !== size) {
this.viewportSize = size;
this.notifyListeners();
if (this.#viewportSize !== size) {
this.#viewportSize = size;
this.#notifyListeners();
}
}

subscribe(callback: (offset: number) => void): () => void {
this.listeners.add(callback);
this.#listeners.add(callback);
return () => {
this.listeners.delete(callback);
this.#listeners.delete(callback);
};
}

private notifyListeners(): void {
this.listeners.forEach((listener) => listener(this.scrollOffset));
#notifyListeners(): void {
this.#listeners.forEach((listener) => listener(this.#scrollOffset));
}
}
4 changes: 2 additions & 2 deletions packages/core/src/utils/calculateVirtualRange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function calculateVirtualRange(
overscan: number,
prevScrollOffset?: number
): VirtualRange {
const startIndex = Math.max(0, Math.floor(scrollOffset / itemSize));
const startIndex = Math.max(0, (scrollOffset / itemSize) | 0);
const endIndex = Math.min(
totalCount - 1,
startIndex + Math.ceil(viewportSize / itemSize)
Expand All @@ -22,7 +22,7 @@ export function calculateVirtualRange(
const overscanStart = isScrollingUp ? overscan * 1.5 : overscan;
const overscanEnd = isScrollingDown ? overscan * 1.5 : overscan;

const renderStart = Math.max(0, Math.floor(startIndex - overscanStart));
const renderStart = Math.max(0, (startIndex - overscanStart) | 0);
const renderEnd = Math.min(totalCount - 1, Math.ceil(endIndex + overscanEnd));

return {
Expand Down
134 changes: 80 additions & 54 deletions packages/core/src/virtualizer/Virtualizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,108 +2,135 @@ import type {
VirtualItem,
VirtualizerState,
VirtualizerOptions,
} from '../types';
import type { LayoutStrategy } from '../strategies/layout/LayoutStrategy';
import type { ScrollSource } from '../strategies/scroll/ScrollSource';
import type { Plugin } from '../plugins/Plugin';
} from "../types";
import type { LayoutStrategy } from "../strategies/layout/LayoutStrategy";
import type { ScrollSource } from "../strategies/scroll/ScrollSource";
import type { Plugin } from "../plugins/Plugin";

export class Virtualizer {
private count: number;
private overscan: number;
private plugins: Plugin[];
private onChange?: (state: VirtualizerState) => void;
#count: number;
readonly #overscan: number;
#plugins: Plugin[];
readonly #onChange?: (state: VirtualizerState) => void;

private layoutStrategy: LayoutStrategy;
private scrollSource: ScrollSource;
readonly #layoutStrategy: LayoutStrategy;
readonly #scrollSource: ScrollSource;

private state: VirtualizerState;
private unsubscribe?: () => void;
#state: VirtualizerState;
readonly #unsubscribe?: () => void;

#prevRenderRange?: { startIndex: number; endIndex: number };
#prevVirtualItems?: VirtualItem[];

constructor(
layoutStrategy: LayoutStrategy,
scrollSource: ScrollSource,
options: VirtualizerOptions
) {
this.layoutStrategy = layoutStrategy;
this.scrollSource = scrollSource;
this.count = options.count;
this.overscan = options.overscan ?? 4;
this.onChange = options.onChange;
this.plugins = [];
this.#layoutStrategy = layoutStrategy;
this.#scrollSource = scrollSource;
this.#count = options.count;
this.#overscan = options.overscan ?? 4;
this.#onChange = options.onChange;
this.#plugins = [];

this.state = this.calculateState();
this.#state = this.#calculateState();

this.plugins.forEach((plugin) => plugin.onInit?.());
this.#plugins.forEach((plugin) => plugin.onInit?.());

this.unsubscribe = this.scrollSource.subscribe(() => {
this.#unsubscribe = this.#scrollSource.subscribe(() => {
this.update();
});
}

addPlugin(plugin: Plugin): void {
this.plugins.push(plugin);
this.#plugins.push(plugin);
plugin.onInit?.();
this.#prevRenderRange = undefined;
this.#prevVirtualItems = undefined;
}

getState(): VirtualizerState {
return this.state;
return this.#state;
}

setCount(count: number): void {
if (this.count !== count) {
this.count = count;
if (this.#count !== count) {
this.#count = count;
this.#prevRenderRange = undefined;
this.#prevVirtualItems = undefined;
this.update();
}
}

update(): void {
const newState = this.calculateState();
const newState = this.#calculateState();

let finalState = newState;
for (const plugin of this.plugins) {
for (const plugin of this.#plugins) {
const result = plugin.beforeStateChange?.(finalState);
if (result) finalState = result;
}

this.state = finalState;
this.#state = finalState;

this.plugins.forEach((plugin) => plugin.afterStateChange?.(this.state));
this.#plugins.forEach((plugin) => plugin.afterStateChange?.(this.#state));

this.onChange?.(this.state);
this.#onChange?.(this.#state);
}

private calculateState(): VirtualizerState {
const scrollOffset = this.scrollSource.getScrollOffset();
const viewportSize = this.scrollSource.getViewportSize();
const totalSize = this.layoutStrategy.getTotalSize(this.count);
#calculateState(): VirtualizerState {
const scrollOffset = this.#scrollSource.getScrollOffset();
const viewportSize = this.#scrollSource.getViewportSize();
const totalSize = this.#layoutStrategy.getTotalSize(this.#count);

let visibleRange = this.layoutStrategy.getVisibleRange(
let visibleRange = this.#layoutStrategy.getVisibleRange(
scrollOffset,
viewportSize,
this.count
this.#count
);

let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.overscan),
endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan),
startIndex: (visibleRange.startIndex - this.#overscan) | 0,
endIndex: (visibleRange.endIndex + this.#overscan) | 0,
};

for (const plugin of this.plugins) {

if (renderRange.startIndex < 0) renderRange.startIndex = 0;
if (renderRange.endIndex > this.#count - 1)
renderRange.endIndex = this.#count - 1;
Comment on lines 93 to +100
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

renderRange를 계산하는 방식이 Math.maxMath.min을 사용하는 이전 코드에 비해 더 장황하고 가독성이 떨어집니다. visibleRange.startIndexoverscan이 모두 정수이므로 | 0 연산은 불필요하며, if 조건문은 Math.max/Math.min과 동일한 역할을 합니다. 코드의 명확성과 간결성을 위해 이전 방식으로 되돌리는 것을 제안합니다.

Suggested change
let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.overscan),
endIndex: Math.min(this.count - 1, visibleRange.endIndex + this.overscan),
startIndex: (visibleRange.startIndex - this.#overscan) | 0,
endIndex: (visibleRange.endIndex + this.#overscan) | 0,
};
for (const plugin of this.plugins) {
if (renderRange.startIndex < 0) renderRange.startIndex = 0;
if (renderRange.endIndex > this.#count - 1)
renderRange.endIndex = this.#count - 1;
let renderRange = {
startIndex: Math.max(0, visibleRange.startIndex - this.#overscan),
endIndex: Math.min(this.#count - 1, visibleRange.endIndex + this.#overscan),
};

Copy link
Owner Author

Choose a reason for hiding this comment

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

헐 진심?


for (const plugin of this.#plugins) {
if (plugin.onRangeCalculated) {
renderRange = plugin.onRangeCalculated(visibleRange, this.count);
renderRange = plugin.onRangeCalculated(visibleRange, this.#count);
}
}

const virtualItems: VirtualItem[] = [];
for (let i = renderRange.startIndex; i <= renderRange.endIndex; i++) {
const start = this.layoutStrategy.getItemOffset(i);
const size = this.layoutStrategy.getItemSize(i);
virtualItems.push({
index: i,
start,
size,
end: start + size,
});
let virtualItems: VirtualItem[];
if (
this.#prevRenderRange &&
this.#prevVirtualItems &&
this.#prevRenderRange.startIndex === renderRange.startIndex &&
this.#prevRenderRange.endIndex === renderRange.endIndex
) {
virtualItems = this.#prevVirtualItems;
} else {
virtualItems = [];
const startIdx = renderRange.startIndex;
const endIdx = renderRange.endIndex;

for (let i = startIdx; i <= endIdx; i++) {
const start = this.#layoutStrategy.getItemOffset(i);
const size = this.#layoutStrategy.getItemSize(i);
virtualItems.push({
index: i,
start,
size,
end: start + size,
});
}

this.#prevRenderRange = renderRange;
this.#prevVirtualItems = virtualItems;
}

return {
Expand All @@ -117,8 +144,7 @@ export class Virtualizer {
}

destroy(): void {
this.unsubscribe?.();
this.plugins.forEach((plugin) => plugin.onDestroy?.());
this.#unsubscribe?.();
this.#plugins.forEach((plugin) => plugin.onDestroy?.());
}
}

9 changes: 7 additions & 2 deletions packages/core/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ export default defineConfig({
minify: "terser",
terserOptions: {
compress: {
passes: 2,
passes: 3,
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log", "console.debug"],
unsafe: true,
unsafe_arrows: true,
unsafe_methods: true,
booleans_as_integers: true,
ecma: 2020,
},
mangle: {
safari10: false,
Expand All @@ -22,7 +27,7 @@ export default defineConfig({
comments: false,
},
},
target: "es2020",
target: "es2022",
outExtension({ format }) {
return {
js: format === "esm" ? ".mjs" : ".cjs",
Expand Down
12 changes: 8 additions & 4 deletions packages/react-native/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ export default defineConfig({
minify: "terser",
terserOptions: {
compress: {
passes: 2,
passes: 3,
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log", "console.debug"],
unsafe: true,
unsafe_arrows: true,
unsafe_methods: true,
booleans_as_integers: true,
ecma: 2020,
},
mangle: {
safari10: false,
Expand All @@ -22,9 +27,8 @@ export default defineConfig({
comments: false,
},
},
target: "es2024",
external: ["react", "react-native"],
noExternal: ["@scrolloop/core", "@scrolloop/shared"],
target: "es2020",
external: ["react", "react-native", "@scrolloop/core", "@scrolloop/shared"],
outExtension({ format }) {
return {
js: format === "esm" ? ".mjs" : ".cjs",
Expand Down
Loading