您现在的位置是:网站首页 > 无限循环与递归(“反正用户会关页面”)文章详情
无限循环与递归(“反正用户会关页面”)
陈川
【
前端综合
】
39506人已围观
6494字
无限循环与递归(“反正用户会关页面”)
前端开发中经常遇到需要重复执行某些操作的场景,比如轮播图自动播放、定时刷新数据等。但如果不小心写出无限循环或递归调用,轻则导致页面卡顿,重则直接让浏览器崩溃。有趣的是,有些开发者会抱着"反正用户会关页面"的心态来处理这些问题,这种想法其实隐藏着不少隐患。
常见的无限循环陷阱
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面板可以捕捉无限循环:
- 开始录制
- 执行可疑代码
- 观察主线程占用情况
- 查找长时间运行的函数调用
添加防护代码:
// 递归保护装饰器
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));
}
}
用户行为的不确定性
假设"用户会关页面"的问题在于:
- 移动端用户习惯按Home键而不是关闭页面
- SPA应用中的路由跳转不会真正卸载页面
- 浏览器可能自动恢复崩溃前的页面状态
// 错误假设示例
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>
);
}
上一篇: 拒绝沟通(“别问我,看代码”)