您现在的位置是:网站首页 > 无限循环与递归(“反正用户会关页面”)文章详情

无限循环与递归(“反正用户会关页面”)

无限循环与递归(“反正用户会关页面”)

前端开发中经常遇到需要重复执行某些操作的场景,比如轮播图自动播放、定时刷新数据等。但如果不小心写出无限循环或递归调用,轻则导致页面卡顿,重则直接让浏览器崩溃。有趣的是,有些开发者会抱着"反正用户会关页面"的心态来处理这些问题,这种想法其实隐藏着不少隐患。

常见的无限循环陷阱

setInterval是最容易产生无限循环的典型场景。比如下面这个计数器代码:

let count = 0;
setInterval(() => {
  count++;
  document.getElementById('counter').textContent = count;
}, 1000);

看起来没问题,但如果用户快速切换页面又返回,可能会产生多个计时器同时运行:

// 错误示例:页面显示时创建计时器,但隐藏时未清除
window.addEventListener('visibilitychange', () => {
  if (document.visibleState === 'visible') {
    setInterval(updateData, 5000); // 每次页面显示都会新建计时器
  }
});

更隐蔽的无限循环可能出现在事件监听中:

// 错误示例:滚动事件触发自身修改
window.addEventListener('scroll', () => {
  if (window.scrollY > 100) {
    window.scrollTo(0, 50); // 触发新的scroll事件
  }
});

递归调用的风险

递归在处理树形结构时很常见,但缺少终止条件就会出问题:

function traverseDOM(node) {
  console.log(node.tagName);
  node.childNodes.forEach(traverseDOM); // 对文本节点也会递归
}

更危险的是间接递归:

// 组件A
function ComponentA() {
  const data = useData();
  return <ComponentB onUpdate={handleUpdate} />;
  
  function handleUpdate() {
    fetchNewData(); // 触发ComponentB重新渲染
  }
}

// 组件B
function ComponentB({ onUpdate }) {
  useEffect(() => {
    onUpdate(); // 回调触发父组件更新
  }, [someDeps]);
}

性能影响实测

用以下代码测试无限循环的影响:

// 测试脚本
function testInfiniteLoop() {
  let i = 0;
  while (true) {
    i++;
    if (i % 100000 === 0) {
      console.log(i);
    }
  }
}

// 在Chrome中执行结果:
// 1. 前几秒还能响应控制台输入
// 2. 约30秒后标签页显示"页面无响应"
// 3. 最终浏览器建议关闭页面

递归深度测试:

let depth = 0;
function recursiveCall() {
  depth++;
  recursiveCall();
}

try {
  recursiveCall();
} catch (e) {
  console.log(`最大递归深度: ${depth}`); 
  // Chrome v102: 约10400次
  // Firefox v101: 约30000次
}

实际业务中的处理策略

对于轮询场景,应该使用可取消的方案:

class SafePoller {
  constructor(callback, interval) {
    this.timer = null;
    this.callback = callback;
    this.interval = interval;
  }
  
  start() {
    this.timer = setTimeout(async () => {
      await this.callback();
      this.start(); // 尾递归优化
    }, this.interval);
  }
  
  stop() {
    clearTimeout(this.timer);
  }
}

// 使用示例
const poller = new SafePoller(fetchData, 5000);
poller.start();
// 页面隐藏时
document.addEventListener('visibilitychange', () => {
  if (document.hidden) poller.stop();
  else poller.start();
});

React中的安全递归渲染:

function TreeView({ nodes, depth = 0 }) {
  if (depth > 10) { // 设置最大深度
    return <div>Too deep</div>;
  }
  
  return (
    <ul>
      {nodes.map(node => (
        <li key={node.id}>
          {node.name}
          {node.children && (
            <TreeView nodes={node.children} depth={depth + 1} />
          )}
        </li>
      ))}
    </ul>
  );
}

调试与监控方案

Chrome DevTools的Performance面板可以捕捉无限循环:

  1. 开始录制
  2. 执行可疑代码
  3. 观察主线程占用情况
  4. 查找长时间运行的函数调用

添加防护代码:

// 递归保护装饰器
function recursionGuard(maxDepth = 100) {
  return function(target, name, descriptor) {
    const original = descriptor.value;
    descriptor.value = function(...args) {
      if (!this.__depth) this.__depth = 0;
      if (this.__depth > maxDepth) {
        throw new Error(`超过最大递归深度 ${maxDepth}`);
      }
      this.__depth++;
      try {
        return original.apply(this, args);
      } finally {
        this.__depth--;
      }
    };
  };
}

class TreeProcessor {
  @recursionGuard(50)
  processNode(node) {
    node.children.forEach(child => this.processNode(child));
  }
}

用户行为的不确定性

假设"用户会关页面"的问题在于:

  1. 移动端用户习惯按Home键而不是关闭页面
  2. SPA应用中的路由跳转不会真正卸载页面
  3. 浏览器可能自动恢复崩溃前的页面状态
// 错误假设示例
function startBackgroundTask() {
  setInterval(() => {
    localStorage.setItem('tempData', generateData());
  }, 100);
  
  // 想着"反正页面关闭就停止了"
}

更好的做法是使用Page Visibility API:

document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    // 释放资源
  } else {
    // 重新初始化
  }
});

框架特定的解决方案

Vue的beforeUnmount钩子:

export default {
  data() {
    return {
      pollInterval: null
    };
  },
  mounted() {
    this.pollInterval = setInterval(this.fetchData, 5000);
  },
  beforeUnmount() {
    clearInterval(this.pollInterval);
  }
};

React的useEffect清理:

useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  
  return () => clearInterval(timer); // 清理函数
}, []);

Angular的ngOnDestroy:

@Component({...})
export class DataComponent implements OnDestroy {
  private destroy$ = new Subject<void>();
  
  constructor() {
    interval(1000)
      .pipe(takeUntil(this.destroy$))
      .subscribe(() => this.updateData());
  }
  
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

浏览器的工作原理视角

浏览器主线程事件循环示意:

Task Queue → Microtask Queue → RAF Callback → Layout → Paint

无限循环会阻塞整个流程:

// 会阻塞渲染的循环
function blockRender() {
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // 模拟长时间运算
  }
  // 5秒后才会执行
  requestAnimationFrame(() => {
    console.log('终于执行了');
  });
}

Web Worker解决方案:

// 主线程
const worker = new Worker('task.js');
worker.postMessage({ cmd: 'start' });

// task.js
self.addEventListener('message', ({ data }) => {
  if (data.cmd === 'start') {
    let i = 0;
    while (true) { // 不会阻塞主线程
      i++;
      if (i % 1000000 === 0) {
        self.postMessage({ count: i });
      }
    }
  }
});

异常处理与恢复

即使发生无限循环,也应尽量优雅降级:

// 全局错误捕获
window.addEventListener('error', (event) => {
  if (event.message.includes('Maximum call stack')) {
    alert('操作太复杂,请简化操作');
    return true; // 阻止默认处理
  }
});

// Web Worker超时控制
function runWithTimeout(workerScript, timeout) {
  const worker = new Worker(workerScript);
  const timer = setTimeout(() => {
    worker.terminate();
    handleTimeout();
  }, timeout);
  
  worker.onmessage = (msg) => {
    clearTimeout(timer);
    handleResult(msg.data);
  };
}

用户体验优化方案

长时间运算的反馈设计:

function HeavyCalculation() {
  const [progress, setProgress] = useState(0);
  
  function calculate() {
    for (let i = 0; i < 100; i++) {
      // 分批处理
      setTimeout(() => {
        doChunkOfWork(i);
        setProgress(i);
      }, 0);
    }
  }
  
  return (
    <div>
      <button onClick={calculate}>开始计算</button>
      <progress value={progress} max="100" />
      {progress > 0 && <button onClick={() => window.location.reload()}>
        取消(刷新页面)
      </button>}
    </div>
  );
}

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步