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

Как предотвратить прекращение работы сайта при рекурсии данных?

2.0 Middle🔥 122 комментариев
#Браузер и сетевые технологии

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как предотвратить прекращение работы сайта при рекурсии данных

Этот вопрос касается защиты приложения от зависаний (freeze) и краша при работе с циклическими ссылками или бесконечной рекурсией. Это важно для стабильности production приложений.

1. Определение проблемы: циклические ссылки

// Проблема: циклические ссылки (circular references)
const person = { name: 'John' };
const job = { title: 'Developer', person: person };
person.job = job; // Теперь есть цикл: person -> job -> person

// Попытка JSON.stringify() завешивает
JSON.stringify(person); // TypeError: Converting circular structure to JSON

// Рекурсивная функция без выхода
function traverse(obj) {
  console.log(obj.name);
  traverse(obj.job.person); // Бесконечная рекурсия!
}

traverse(person); // Stack overflow -> краш браузера

2. Детектирование циклических ссылок

// Способ 1: Отслеживание посещённых объектов
function detectCycle(obj, visited = new WeakSet()) {
  if (typeof obj !== 'object' || obj === null) {
    return false;
  }
  
  if (visited.has(obj)) {
    return true; // Цикл найден!
  }
  
  visited.add(obj);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (detectCycle(obj[key], visited)) {
        return true; // Цикл в свойстве
      }
    }
  }
  
  visited.delete(obj); // Удаляем при возврате для других веток
  return false;
}

const person = { name: 'John' };
const job = { title: 'Developer', person };
person.job = job;

console.log(detectCycle(person)); // true - есть цикл
console.log(detectCycle({ name: 'Jane' })); // false - цикла нет

3. Безопасная сериализация с циклами

// Способ 1: JSON.stringify с replacer
function safeStringify(obj, maxDepth = 3) {
  const visited = new WeakSet();
  
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (visited.has(value)) {
        return '[Circular Reference]'; // Заменяем цикл
      }
      visited.add(value);
    }
    return value;
  });
}

const person = { name: 'John' };
const job = { title: 'Developer', person };
person.job = job;

console.log(safeStringify(person));
// {"name":"John","job":{"title":"Developer","person":"[Circular Reference]"}}

// Способ 2: Ограничение глубины
function stringifyWithDepth(obj, maxDepth = 5, currentDepth = 0) {
  if (currentDepth >= maxDepth) {
    return '[Max Depth Reached]';
  }
  
  if (typeof obj !== 'object' || obj === null) {
    return obj;
  }
  
  if (Array.isArray(obj)) {
    return obj.map(item => stringifyWithDepth(item, maxDepth, currentDepth + 1));
  }
  
  const result = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = stringifyWithDepth(obj[key], maxDepth, currentDepth + 1);
    }
  }
  return result;
}

4. Безопасная рекурсивная обработка

// Способ 1: С использованием Set для отслеживания
function traverse(obj, callback, visited = new WeakSet()) {
  if (typeof obj !== 'object' || obj === null) {
    return;
  }
  
  if (visited.has(obj)) {
    return; // Уже посещали, не ходим снова
  }
  
  visited.add(obj);
  callback(obj);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      traverse(obj[key], callback, visited);
    }
  }
}

const person = { name: 'John', age: 30 };
const job = { title: 'Developer', person };
person.job = job;

traverse(person, (node) => {
  console.log(node);
});
// Печатает person один раз, потом job один раз, не зависает

// Способ 2: Ограничение глубины рекурсии
function processTree(node, depth = 0, maxDepth = 10) {
  if (depth > maxDepth) {
    console.warn('Max depth reached');
    return;
  }
  
  if (!node) return;
  
  console.log(`Depth ${depth}:`, node.name);
  
  if (node.children) {
    node.children.forEach(child => processTree(child, depth + 1, maxDepth));
  }
}

const tree = {
  name: 'root',
  children: [{ name: 'child', children: [] }],
};

processTree(tree);

5. React компонент - избежание infinite loops

import React, { useState, useEffect, useMemo } from 'react';

// Проблема: infinite loop в useEffect
function BadComponent() {
  const [data, setData] = useState([]);
  
  useEffect(() => {
    // ПЛОХО: каждый раз создаётся новый массив
    setData([...data, { id: 1 }]); // Вызовет useEffect снова!
  }, [data]); // data зависит от себя
  
  return <div>{data.length}</div>;
}

// Решение 1: Правильные зависимости
function GoodComponent() {
  const [data, setData] = useState([]);
  const [loaded, setLoaded] = useState(false);
  
  useEffect(() => {
    if (!loaded) {
      setData([{ id: 1 }, { id: 2 }]);
      setLoaded(true);
    }
  }, [loaded]); // Зависимость имеет смысл
  
  return <div>{data.length}</div>;
}

// Решение 2: Использование useCallback
function Component() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // Стабильная функция
  
  useEffect(() => {
    // Безопасно использовать handleClick
    const timer = setInterval(handleClick, 1000);
    return () => clearInterval(timer);
  }, [handleClick]);
  
  return <div>Count: {count}</div>;
}

// Решение 3: Мемоизация зависимостей
function UserList({ users }) {
  const memoizedUsers = useMemo(() => users, [users]);
  
  useEffect(() => {
    // Теперь effect запустится только если users действительно изменились
    console.log('Users updated');
  }, [memoizedUsers]);
  
  return <ul>{memoizedUsers.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

6. GraphQL и API: обработка циклических данных

// Проблема: GraphQL query может вернуть циклические данные
const query = `
  query GetUser {
    user(id: "1") {
      name
      friends {
        name
        friends {
          name
          friends { ... } // Цикл потенциально бесконечный
        }
      }
    }
  }
`;

// Решение: Используй fragment depth limit
const safeQuery = `
  query GetUser {
    user(id: "1") {
      name
      friends {
        name
        # Здесь не запрашиваем friends чтобы избежать цикла
      }
    }
  }
`;

// Альтернатива: Обрабатываем на фронте
function normalizeGraphQLResponse(data, maxDepth = 3) {
  const visited = new WeakSet();
  
  function normalize(obj, depth = 0) {
    if (depth > maxDepth) return null;
    if (typeof obj !== 'object' || obj === null) return obj;
    
    if (visited.has(obj)) return null; // Цикл - не включаем
    visited.add(obj);
    
    if (Array.isArray(obj)) {
      return obj.map(item => normalize(item, depth + 1));
    }
    
    const result = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        result[key] = normalize(obj[key], depth + 1);
      }
    }
    return result;
  }
  
  return normalize(data);
}

7. Лучшие практики

// ✅ Всегда используй WeakSet для отслеживания посещённых объектов
const visited = new WeakSet(); // не память leak

// ✅ Устанавливай maxDepth для рекурсии
function process(obj, depth = 0, maxDepth = 20) {
  if (depth > maxDepth) throw new Error('Max recursion depth exceeded');
}

// ✅ Тестируй с циклическими данными
const testCyclic = { a: { b: { c: {} } } };
testCyclic.a.b.c.back = testCyclic; // Цикл для тестирования

// ✅ Используй error boundaries в React
class ErrorBoundary extends React.Component {
  componentDidCatch(error, info) {
    console.error('Render error:', error);
  }
  render() {
    return this.props.children;
  }
}

Ответ на интервью

«Для предотвращения зависания от циклических ссылок я использую несколько подходов:

  1. WeakSet для отслеживания: При рекурсии отслеживаю посещённые объекты и не захожу в них дважды
  2. Ограничение глубины: Устанавливаю maxDepth для защиты от бесконечной рекурсии
  3. JSON.stringify с replacer: Перехватываю циклы и заменяю на placeholder
  4. Правильные useEffect зависимости: В React избегаю infinite loops через правильный массив зависимостей
  5. Тестирование: Всегда тестирую циклические данные в unit тестах

Главное — предусмотреть проблему на этапе проектирования, а не бороться с ней в production.»