2015-10-23 2 views
4

У меня есть рекурсивная функция с асинхронным интерфейсом, который, скорее всего, превысит пределы глубины стеки при вызове:Как ловушка переполнение стека ошибки

function f(x, cb) { 
    if (x === 0) { 
     cb(); 
    } else { 
     f(x - 1, cb); 
    } 
} 

f(1e6, function() { 
    console.log('done'); 
}); // BOOM 

(и да, она должна быть рекурсивными, переписыванием, чтобы быть итеративным является нежизнеспособны).

я могу решить эту проблему, выполнив рекурсивный вызов асинхронно (например, с помощью setTimeout или window.postMessage, которая якобы быстрее):

function f(x, cb) { 
    if (x === 0) { 
     cb(); 
    } else { 
     setTimeout(function() { 
      f(x - 1, cb); 
     }, 0); 
    } 
} 

f(1e6, function() { 
    console.log('done'); 
}); // ok 

Но это значительно медленнее. Поэтому я хочу выполнить асинхронный вызов только тогда, когда он в противном случае вызовет переполнение стека. Что-то вроде

function f(x, cb) { 
    if (x === 0) { 
     cb(); 
    } else { 
     if (getCurrentStackDepth() == getMaxStackDepth() - 42) 
      setTimeout(function() { 
       f(x - 1, cb); 
      }, 0); 
     } else { 
      f(x - 1, cb); 
     } 
    } 
} 

или, если это не представляется возможным, по крайней мере, определить, когда происходит переполнение, и повторите попытку асинхронно. Что-то по строкам

function f(x, cb) { 
    if (x === 0) { 
     cb(); 
    } else { 
     try { 
      f(x - 1, cb); 
     } catch (e) { 
      if (isStackOverflowError(e)) { 
       setTimeout(function() { 
        f(x - 1, cb); 
       }, 0); 
      } else { 
       throw e; 
      } 
     } 
    } 
} 

Как это можно сделать? Решение через Function.prototype.caller неприемлемо, так как я в строгом режиме es5-es6. Я бы предпочел портативное решение, но на самом деле он нужен только для хром.

+0

Гораздо проще переписать рекурсию как цикл. И да, вы сказали, что это должно быть рекурсивным, но когда вы используете 'setTimeout', это не будет рекурсивным. – Kenney

+0

@ Kenney это пример, у меня на самом деле около десятка взаимно-рекурсивных асинхронных функций ветвления. –

+0

Хорошо. Прибегнуть к 'setTimeout', чтобы избежать stackoverflow - это плохое решение, IMHO. Ваша исходная функция в основном сводится к 'for (; x> 0; x--) {}; CB(); '. (Кстати, вы понимаете, что ваша верхняя функция не является асинхронной?) – Kenney

ответ

1

К сожалению, у браузера не обнаружен метод (JS), чтобы проверить размер стека вызовов, как вы сказали getCurrentStackDepth. Я попытался выяснить это и не мог получить никакой информации, связанной с этим.

Последнее решение может быть одним из способов решения этой проблемы. Тип объекта исключения, созданного JS-движком для размера стека, превышает RangeError. Таким образом, используя эту информацию и сообщение об исключении, мы можем написать метод isStackOverflowError, как показано ниже

function isStackOverflowError(e) { 
    return (e instanceof RangeError) && /.+stack.+size.+/.test(e.message); 
} 

Кроме того, это более высокую производительность по сравнению с примером-2, но до сих пор не слишком быстро. Я попытаюсь обновить, если найду лучший способ решить эту проблему.

0

Это один из способов сделать это:

function f(x, cb) { 
    if (x === 0) { 
     cb(); 
    } else { 
     try { 
     f(x - 1, cb); 
     } catch (e) { 
     setTimeout(function() { f(x-1, cb); }, 0); 
     } 
    } 
} 

f(100000,function(){console.log("Done!")}) 

Однако, переполнение стека ошибка программирования, и я не могу рекомендовать решение это так. Намного лучше реорганизовать рекурсивный код на итеративный, поскольку они функционально эквивалентны. Итеративная версия будет использовать «стек», выделенный из кучи, который может быть намного больше, чем обычный стек процесса.

Для получения дополнительной информации см. Recursion versus iteration.

+0

Почему downvote? Любая конструктивная обратная связь? – Kenney

Смежные вопросы