Проблема
Некоторое время назад в работе над клиентской (javascript) частью движка josi возникла, кстати, достаточно часто встречающаяся проблема переполнения стека:
Uncaught RangeError: Maximum call stack size exceeded (google chrome)
В статье рассматривается решение без использования setTimout или setInterval.
Суть
Причина такого поведения известна и понятна, и в той или иной форме всегда вызвана следующим. Классическая(прямая) рекурсия порождает цепочку последовательных вызовов, что соответственно ведет к наполнению стека вызовов, однако, стек вызовов браузера достаточно мал, в chrome на момент тестирования это 500 вызовов, в safari, если не ошибаюсь, тоже. В любом случае- это предельное значение, а значит его можно превысить и получить exception. Естественно, столь долгое выполнение кода не желательно в принципе, и этого стоит избегать. И все же лично мне не хочеться полагаться на удачу, не смотря на то, что ситуация в которой пришлось столкнуться с проблемой на продакшен возникнуть не должна, я потратил время на изучение данного вопроса.
Решение
Классическим решением (имею ввиду подавляющее количество статей предлагающих его) является использование косвенной рекурсии посредством: setTimeout либо setInterval.
В качестве примера приведу простенькую рекурсивную функция, единственное назначение которой рано или поздно вернуть Вам предел размера стека вместе с exeption о превышении этого предела…
function f(args) { var self=this; var k=args.k; //вызываем себя же try { f({k:k+1}); } catch(ex) { alert(k); } }
та же бесполезная функция, но теперь теоретически бесконечная, разве что k переполнится
function f(args) { var self=this; var k=args.k; //косвенно вызываем себя же, через посредника setTimeout setTimeout(function(){ f({k:k+1}) }, 0); }
Текущая функция сразу завершается за счет использования для рекурсии посредника setTimout, а следующий вызов выполняется по событию.
Отрицательной стороной такого подхода является его крайне низкая производительность, несмотря на то, что мы указываем нулевую задержку. Вызвана функция будет в зависимости от браузера в среднем не раньше чем через 10 мс. Но ведь мы боремся с превышением стека вызовов, а значит наша функция вызывается сотни раз, что означает потерю в производительности ~1 с на каждые 100 вызовов. Детальное тестирование нашел тут.
Самое простое, что пришло в голову — организовать симбиоз из попеременного использования прямого и косвенного вызовов, чтобы при достижении некоторого значения счетчика прерывать стек косвенным вызовом. Отчасти такое решение сейчас и используется. Но здесь тоже все не так просто, особенно если рекурсия представлена петлей из нескольких функций.
Вот простенький пример отражающий суть такого решения:
var max_call_i=300; function f(args) { var self=this; var k=args.k; var call_i=args.call_i //alert(k); if (call_i>=max_call_i) { //косвенно вызываем себя же, через посредника setTimeout setTimeout(function(){ f({k:k+1, call_i:call_i+1}) }, 0); } else { //напрямую вызываем себя же f({k:k+1, call_i:call_i+1}); } }
В моем коде проблема возникла в шаблонизаторе, который как раз незадолго до этого был переписан согласно новой парадигме. Не хотелось отказываться от принятой архитектуры. В тоже время реальное падение производительности составило 20-30% — что было просто чудовищно. Предложенное выше решение тоже не идеал: сохранялось падение производительности на 5-7%. Это меня не устроило: много гуглил, и напал на то, что нужно.
А это тест от туда же, из которого видно что, предложенный подход, в сравнении с setTimeout 0, гораздо более производительный, что на практике дало не более 3% падения производительности в моем случае…
Данное решение основано на связке window.postMessage и element.addEventListener, для меня достаточно кроссбраузерно (ie8+).
Я переработал функцию из приведенной выше статьи в AMD модуль. Возможно, кому-то это будет полезным…
define([], function () { return { args: //аргументы { indirect_call: { f_arr:[], msg_name:"indirect_call-message", handler_f:null, } }, /*** работа с событиями ***/ f_indirect_call:function(f) { var self=this; //если это первый вызов то создаем обработчик события и привязываем к window if (t_uti.f_is_empty(self.args.indirect_call.handler_f)) { //создаем обработчик события self.args.indirect_call.handler_f=function(event) { if (event.source == window && event.data == self.args.indirect_call.msg_name) { event.stopPropagation(); if (self.args.indirect_call.f_arr.length> 0) { var f = self.args.indirect_call.f_arr.shift(); f(); } } } window.addEventListener("message", self.args.indirect_call.handler_f, true); } self.args.indirect_call.f_arr.push(f); window.postMessage(self.args.indirect_call.msg_name, "*"); }, }; });
ссылка на оригинал статьи http://habrahabr.ru/post/167033/
Добавить комментарий