JavaScript 異步編程指南 - 如何用異步任務解決遞歸棧溢出?
在編程中使用遞歸,如果沒有控制好代碼的執行邊界或過多層級的遞歸調用,就會造成棧溢出錯誤,就像下面展示的這段錯誤堆棧。
- RangeError: Maximum call stack size exceeded
- at fn (/xxx/test.js:2:3)
- at fn (/xxx/test.js:7:10)
為什么遞歸會造成堆棧溢出?
函數運行會有一個執行棧,每次調用會做入棧操作,保存一些局部變量、函數參數、當前程序的運行狀態等,這些信息都會保存在??臻g里,而??臻g的存儲是一段連續的內存地址,有大小限制。
以下是一段遞歸調用的簡單示例。
- function fn(i) {
- i--;
- if (i < 1) {
- return;
- }
- return fn(i);
- }
- fn(20000);
以下通過 gif 動圖展示了上述代碼的執行過程,當在主線程上調用 fn 函數后,不斷的做壓棧操作,而棧空間也在不斷的增加,直到達到最大的??臻g限制,程序報錯 “Maximum call stack size exceeded”。

javascript-recursion-stack-overflow (1).gif
使用異步解決棧溢出問題解
決遞歸造成的棧溢出問題,一種方法是可以使用 JavaScript 中的異步任務,也是借助了事件循環機制。宏任務有 setTimeout、Node.js 環境下的 setImmediate,微任務有 Promise、queueMicrotask。
修改代碼,在 setTimeout 函數里遞歸調用。
- function fn(i) {
- i--;
- if (i < 1) {
- return;
- }
- setTimeout(function() {
- fn(i);
- }, 0);
- }
- fn(20000);
運行效果如下所示:

javascript-async-recursion.gif
當首次調用 fn(2000) 時,創建一個調用棧,函數內部調用 setTimeout 函數后會立即返回,當前的調用棧就結束了,傳入的回調 **function() { fn(i) }** 還沒有執行,主線程不會在這里等待,也不會形成層層嵌套的調用鏈。
定時器函數由宿主環境實現,當將來的某個時間點計時器時間到達后,宿主環境會將 timer 函數封裝為一個事件放入 “任務隊列” 中,事件循環檢測到任務隊列有可執行的任務,就拿出來執行,之后再次調用 fn(i) 創建新的調用棧,反復循環。
還可以通過微任務實現,微任務有個缺點是當調度大量的微任務時雖然不會導致調用棧溢出,但也會導致和同步任務相同的性能缺陷,后面的任務得不到執行,瀏覽器的渲染工作也會被阻止,直到所有的微任務執行完畢。
總結
這個問題通過結合異步任務來解決遞歸造成的棧溢出問題,也可以做為事件循環的一個例子來學習,更好的掌握同步任務、異步之間的調度關系。
在程序中使用遞歸還是要謹慎的,若控制不好邊界,很容易造成 “棧溢出”。除了改為異步任務調用外,還可將遞歸改為循環迭代、尾遞歸優化等。