← Назад к вопросам

Реализовать функцию memoize

1.7 Middle🔥 241 комментариев
#JavaScript Core

Условие

Напишите функцию memoize(fn), которая кэширует результаты вызова функции.

Требования

  1. При повторном вызове с теми же аргументами возвращать кэшированный результат
  2. Поддержка функций с любым количеством аргументов
  3. Корректная работа с примитивами и объектами

Примеры

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

  1. Intersection Observer API — эффективнее, чем обработка scroll события
  2. rootMargin — предзагрузка за пределами видимой области
  3. Защита от гонки запросов — только один запрос одновременно
  4. Виртуализация — отрисовка только видимых элементов
  5. Управление состояниями — loading, error, hasMore

Best Practices

  1. Использовать Intersection Observer вместо scroll событий
  2. Защищать от множественных одновременных запросов
  3. Виртуализировать список для больших наборов данных
  4. Показывать индикаторы состояния
  5. Обрабатывать ошибки загрузки

Рекомендации для собеседования

  1. Начните с базового компонента
  2. Объясните Intersection Observer API
  3. Покажите защиту от гонки запросов
  4. Добавьте виртуализацию
  5. Обсудите производительность

Итог: Базовый компонент с Intersection Observer - простой и эффективный. Виртуализация для больших списков.