← Назад к вопросам
Реализовать функцию memoize
1.7 Middle🔥 241 комментариев
#JavaScript Core
Условие
Напишите функцию memoize(fn), которая кэширует результаты вызова функции.
Требования
- При повторном вызове с теми же аргументами возвращать кэшированный результат
- Поддержка функций с любым количеством аргументов
- Корректная работа с примитивами и объектами
Примеры
function slowAdd(a, b) {
console.log("Computing...");
return a + b;
}
var memoizedAdd = memoize(slowAdd);
memoizedAdd(1, 2); // "Computing..." -> 3
memoizedAdd(1, 2); // 3 (из кэша, без Computing)
memoizedAdd(2, 3); // "Computing..." -> 5
Бонус
Добавьте возможность ограничения размера кэша (LRU cache).
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Задача на бесконечный скролл с Intersection Observer API — показывает понимание производительности и современных браузерных API.
Решение 1: Базовый бесконечный скролл
import React, { useState, useEffect, useRef, useCallback } from "react";
interface InfiniteScrollProps<T> {
loadMore: (page: number) => Promise<T[]>;
renderItem: (item: T, index: number) => React.ReactNode;
hasMore?: boolean;
offset?: number;
}
function InfiniteScroll<T>({
loadMore,
renderItem,
hasMore = true,
offset = 500,
}: InfiniteScrollProps<T>) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const observerTarget = useRef<HTMLDivElement>(null);
const loadingRef = useRef(false);
// Загрузка данных
const handleLoadMore = useCallback(async () => {
if (loadingRef.current || !hasMore) return;
loadingRef.current = true;
setLoading(true);
try {
const newItems = await loadMore(page);
setItems((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 1);
setError(null);
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setLoading(false);
loadingRef.current = false;
}
}, [page, loadMore, hasMore]);
// Intersection Observer
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading && hasMore) {
handleLoadMore();
}
},
{ rootMargin: `${offset}px` }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [handleLoadMore, loading, hasMore, offset]);
return (
<div className="infinite-scroll">
<div className="items-list">
{items.map((item, idx) => (
<div key={idx} className="item">
{renderItem(item, idx)}
</div>
))}
</div>
{/* Sentinel элемент для наблюдения */}
<div ref={observerTarget} className="sentinel">
{loading && <div className="loader">Загрузка...</div>}
{error && <div className="error">Ошибка: {error.message}</div>}
{!hasMore && !loading && <div className="end-message">Больше нет данных</div>}
</div>
</div>
);
}
export default InfiniteScroll;
Использование
function PostList() {
const loadMorePosts = async (page: number) => {
const response = await fetch(`/api/posts?page=${page}&limit=20`);
if (!response.ok) throw new Error("Ошибка загрузки");
return response.json();
};
return (
<InfiniteScroll
loadMore={loadMorePosts}
renderItem={(post) => <PostCard post={post} />}
hasMore={true}
offset={500}
/>
);
}
Решение 2: С виртуализацией списка
interface VirtualizedInfiniteScrollProps<T> extends InfiniteScrollProps<T> {
itemHeight: number;
visibleCount?: number;
}
function VirtualizedInfiniteScroll<T>({
loadMore,
renderItem,
itemHeight,
visibleCount = 10,
hasMore = true,
offset = 500,
}: VirtualizedInfiniteScrollProps<T>) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const loadingRef = useRef(false);
const handleLoadMore = useCallback(async () => {
if (loadingRef.current || !hasMore) return;
loadingRef.current = true;
setLoading(true);
try {
const newItems = await loadMore(page);
setItems((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 1);
} catch (err) {
setError(err instanceof Error ? err : new Error("Unknown error"));
} finally {
setLoading(false);
loadingRef.current = false;
}
}, [page, loadMore, hasMore]);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const target = e.currentTarget;
setScrollTop(target.scrollTop);
// Загрузка при приближении к концу
const scrollPercent = (target.scrollTop + target.clientHeight) / target.scrollHeight;
if (scrollPercent > 0.8 && !loading && hasMore) {
handleLoadMore();
}
};
// Вычисляем видимый диапазон
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = startIndex + visibleCount;
const visibleItems = items.slice(startIndex, endIndex + 1);
const offsetY = startIndex * itemHeight;
return (
<div
ref={containerRef}
className="virtualized-scroll"
style={{ height: "600px", overflow: "auto", position: "relative" }}
onScroll={handleScroll}
>
{/* Пустое пространство до видимых элементов */}
<div style={{ height: offsetY }} />
{/* Видимые элементы */}
{visibleItems.map((item, idx) => (
<div key={startIndex + idx} style={{ height: itemHeight }}>
{renderItem(item, startIndex + idx)}
</div>
))}
{/* Пустое пространство после видимых элементов */}
<div style={{ height: Math.max(0, (items.length - endIndex) * itemHeight) }} />
{/* Состояние загрузки */}
{loading && <div className="loader">Загрузка...</div>}
{error && <div className="error">Ошибка: {error.message}</div>}
</div>
);
}
Решение 3: С защитой от гонки запросов
class RequestManager {
private queue: Promise<any> = Promise.resolve();
private abortControllers: Map<number, AbortController> = new Map();
async enqueue<T>(fn: () => Promise<T>, pageId: number): Promise<T> {
// Отменяем предыдущий запрос для этой страницы
const existing = this.abortControllers.get(pageId);
if (existing) {
existing.abort();
}
const controller = new AbortController();
this.abortControllers.set(pageId, controller);
this.queue = this.queue.then(() => fn());
return this.queue;
}
}
function SafeInfiniteScroll<T>(props: InfiniteScrollProps<T>) {
const requestManager = useRef(new RequestManager());
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(false);
const observerTarget = useRef<HTMLDivElement>(null);
const pendingRef = useRef(false);
const handleLoadMore = useCallback(async () => {
if (pendingRef.current) return;
pendingRef.current = true;
setLoading(true);
try {
const newItems = await requestManager.current.enqueue(
() => props.loadMore(page),
page
);
setItems((prev) => [...prev, ...newItems]);
setPage((prev) => prev + 1);
} catch (err) {
if (err instanceof Error && err.name !== "AbortError") {
console.error("Load error:", err);
}
} finally {
setLoading(false);
pendingRef.current = false;
}
}, [page, props]);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loading) {
handleLoadMore();
}
},
{ rootMargin: "500px" }
);
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [handleLoadMore, loading]);
return (
<div>
<div className="items">
{items.map((item, idx) => (
<div key={idx}>{props.renderItem(item, idx)}</div>
))}
</div>
<div ref={observerTarget} className="sentinel">
{loading && <div>Загрузка...</div>}
</div>
</div>
);
}
Key Features
- Intersection Observer API — эффективнее, чем обработка scroll события
- rootMargin — предзагрузка за пределами видимой области
- Защита от гонки запросов — только один запрос одновременно
- Виртуализация — отрисовка только видимых элементов
- Управление состояниями — loading, error, hasMore
Best Practices
- Использовать Intersection Observer вместо scroll событий
- Защищать от множественных одновременных запросов
- Виртуализировать список для больших наборов данных
- Показывать индикаторы состояния
- Обрабатывать ошибки загрузки
Рекомендации для собеседования
- Начните с базового компонента
- Объясните Intersection Observer API
- Покажите защиту от гонки запросов
- Добавьте виртуализацию
- Обсудите производительность
Итог: Базовый компонент с Intersection Observer - простой и эффективный. Виртуализация для больших списков.