Ситуация: заказчик попросил разместить на странице кликабельную картинку, а чтобы на неё обратило внимание больше посетителей, попросил сделать вокруг неё моргающую рамку. Логика моргания в скрипте очень простая:
- В первой функции находим на странице нужный элемент.
- Добавляем рамку с какой-то задержкой (чтобы она какое-то время была на экране).
- Вызываем функцию убирания рамки.
- Внутри второй функции находим тот же элемент на странице.
- Убираем рамку с задержкой.
- Вызываем первую функцию добавления рамки.
Код простой, поэтому делаем всё в одном файле:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pulse</title>
<style type="text/css">
/* рамка, которая будет моргать */
.pulse { box-shadow: 0px 0px 4px 4px #AEA79F; }
</style>
</head>
<body>
<div id="pulseDiv">
<a href="#">
<div id="advisersDiv">
<img src="https://thecode.media/wp-content/uploads/2020/08/photo_2020-08-05-12.04.57.jpeg">
</div>
</a>
</div>
<!-- подключаем jQuery -->
<script src="https://yastatic.net/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<!-- наш скрипт -->
<script type="text/javascript">
// добавляем рамку
function fadeIn() {
// находим нужный элемент и добавляем рамку с задержкой
$('#pulseDiv').find('div#advisersDiv').delay(400).addClass("pulse");
// затем убираем рамку
fadeOut();
};
// убираем рамку
function fadeOut() {
// находим нужный элемент и убираем рамку с задержкой
$('#pulseDiv').find('div#advisersDiv').delay(400).removeClass("pulse");
// затем добавляем
fadeIn();
};
// запускаем моргание рамки
fadeIn();
</script>
</body>
</html>
Но при открытии страницы в браузере мы видим, что ничего не моргает, а в консоли появилась ошибка:
❌ Uncaught RangeError: Maximum call stack size exceeded
Что это значит: в браузере произошло переполнение стека вызовов и из-за этого он не может больше выполнять этот скрипт.
Переполнения стека простыми словами означает вот что:
- Когда компьютер что-то делает, он это делает последовательно —
1
,2
,3
,4
. - Иногда ему нужно отвлечься от одного и сходить сделать что-то другое — а, б, в, г, д. Получается что-то вроде
1
,2
,3 → а
,б
,в
,г
,д → 4
. - Вот эти переходы
3 → а
ид → 4
— это компьютеру нужно запомнить, что он выполнял пункт 3, и потом к нему вернуться. - Каждое запоминание, что компьютер бросил и куда ему нужно вернуться, — это называется «вызов».
- Вызовы хранятся в стеке вызовов. Это стопка таких ссылок типа «когда закончишь вот это, вернись туда».
- Стек не резиновый и может переполняться.
Что делать с ошибкой Uncaught RangeError: Maximum call stack size exceeded
Эта ошибка — классическая ошибка переполнения стека во время выполнения рекурсивных функций.
Рекурсия — это когда мы вызываем функцию внутри самой себя, но чуть с другими параметрами. Когда параметр дойдёт до конечного значения, цепочка разматывается обратно и функция собирает вместе все значения. Это удобно, когда у нас есть чёткий алгоритм подсчёта с понятными правилами вычислений.
В нашем случае рекурсия возникает, когда в конце обеих функций мы вызываем другую:
- Функции начинают бесконтрольно вызывать себя бесконечное число раз.
- Стек вызовов начинает запоминать вызов каждой функции, чтобы, когда она закончится, вернуться к тому, что было раньше.
- Стек — это определённая область памяти, у которой есть свой объём.
- Вызовы не заканчиваются, и стек переполняется — в него больше нельзя записать вызов новой функции, чтобы потом вернуться обратно.
- Браузер видит всё это безобразие и останавливает скрипт.
То же самое будет, если мы попробуем запустить простую рекурсию слишком много раз:
Как исправить ошибку Uncaught RangeError: Maximum call stack size exceeded
Самый простой способ исправить эту ошибку — контролировать количество рекурсивных вызовов, например проверять это значение на входе. Если это невозможно, то стоит подумать, как можно переделать алгоритм, чтобы обойтись без рекурсии.
В нашем случае проблема возникает из-за того, что мы вызывали вторые функции бесконтрольно, поэтому они множились без ограничений. Решение — ограничить вызов функции одной секундой — так они будут убираться из стека и переполнения не произойдёт:
<script type="text/javascript">
// добавляем рамку
function fadeIn() {
// находим нужный элемент и добавляем рамку с задержкой
$('#pulseDiv').find('div#advisersDiv').delay(400).addClass("pulse");
// через секунду убираем рамку
setTimeout(fadeOut,1000)
};
// убираем рамку
function fadeOut() {
// находим нужный элемент и убираем рамку с задержкой
$('#pulseDiv').find('div#advisersDiv').delay(400).removeClass("pulse");
// через секунду добавляем рамку
setTimeout(fadeIn,1000)
};
// запускаем моргание рамки
fadeIn();
</script>