在编程中使用递归,如果没有控制代码的执行边界或者递归调用的层级过多,就会造成栈溢出错误,如下图的错误栈。RangeError:Maximumcallstacksizeexceededatfn(/xxx/test.js:2:3)atfn(/xxx/test.js:7:10)为什么递归会导致栈溢出?函数运行时会有一个执行栈,每次调用都会压入栈中操作,保存一些局部变量,函数参数,当前程序的运行状态等,这些信息都会保存在栈空间中,而栈空间的存储是一个连续的内存地址,有大小限制。下面是一个递归调用的简单例子。functionfn(i){i--;if(i<1){return;}returnfn(i);}fn(20000);下面通过gif动图展示上面代码的执行过程,当在主线程上调用fn函数后,不断的进行入栈操作,栈空间也不断增加,直到最大栈空间限制为达到,程序报错“超出最大调用堆栈大小”。javascript-recursion-stack-overflow(1).gif使用异步解决栈溢出问题解决递归引起的栈溢出问题。一种方法是使用JavaScript中的异步任务,它也使用了事件循环机制。宏任务包括Node.js环境下的setTimeout和setImmediate,微任务包括Promise和queueMicrotask。修改代码,在setTimeout函数中递归调用。functionfn(i){i--;if(i<1){return;}setTimeout(function(){fn(i);},0);}fn(20000);运行效果如下:javascript-async-recursion.gif第一次调用fn(2000)时,创建调用栈,函数内部调用setTimeout函数后立即返回,当前调用栈结束了,传入的回调**function(){fn(i)}**还没有执行完,主线程不会在这里等待,也不会形成嵌套的调用链。定时器功能由宿主环境实现。当定时器时间到达以后的某个时间点时,宿主环境会将定时器函数封装为一个事件,放入“任务队列”中。事件循环检测到任务队列中有可执行任务,取出并执行,然后再次调用fn(i)创建新的调用栈,如此循环。也可以通过微任务来实现。微任务的缺点是虽然调度了大量的微任务,虽然调用栈不会溢出,但是也会造成和同步任务一样的性能缺陷。后续任务无法执行,浏览器的渲染工作也会被阻塞,直到所有微任务执行完毕。总结这个问题结合异步任务解决递归导致的栈溢出问题。也可以作为事件循环的例子来学习,更好的把握同步任务和异步任务的调度关系。在程序中使用递归还是要谨慎。如果边界控制不好,很容易造成“栈溢出”。除了改成异步任务调用,递归还可以改成循环迭代,尾递归优化等。
