目录
一、前言
在HOW - 前端定时器实践(含防抖、interval 模拟)中我们简单了解过 JavaScript 中的计时器以及潜在问题,包括防抖和节流的应用。那今天我们进一步来学习计时器实践和注意事项。
二、具体介绍
在前端项目中使用 setTimeout
和 setInterval
执行定时任务是很常见的,但在实践中需要注意以下几个方面,以确保代码的正确性和性能:
1. 清理定时器
在组件卸载或不再需要定时器时,务必清理定时器,以避免内存泄漏和不必要的计算:
比如在 React 项目中,使用 clearTimeout
和 clearInterval
: 在组件的 useEffect
钩子中,可以返回一个清理函数来清除定时器。
示例:
import React, { useEffect } from 'react';
const TimerComponent: React.FC = () => {
useEffect(() => {
const timerId = setInterval(() => {
console.log('Interval task');
}, 1000);
return () => clearInterval(timerId); // 清理定时器
}, []);
return <div>Timer Component</div>;
};
2. 避免使用过多的定时器
大量使用定时器可能会导致性能问题,特别是在浏览器中。如果你需要多个定时任务,考虑合并它们或者使用更合适的机制,如 requestAnimationFrame
或者 Web Workers
。
3. 处理定时器的延迟和准确性
-
setTimeout
和setInterval
的延迟不一定精确: 特别是在 CPU 使用率很高的情况下,定时器可能会有延迟。setInterval
的实际间隔可能会比预期长。 -
适当使用
Date.now()
或performance.now()
: 如果你需要更精确的时间管理,可以使用Date.now()
或performance.now()
来计算实际的时间间隔。
示例:
const timerId = setInterval(() => {
const now = performance.now();
console.log(`Interval at ${now}`);
}, 1000);
4. 避免在定时器中访问过期的状态
如果定时器引用了组件的状态或属性,确保在定时器执行时这些引用仍然有效。否则,你可能会遇到状态过期的问题。
例如,在 React 项目中,使用 useRef
或在清理函数中取消定时器可以避免这种问题。
示例:
import React, { useEffect, useRef } from 'react';
const TimerComponent: React.FC = () => {
const countRef = useRef(0);
useEffect(() => {
const timerId = setInterval(() => {
countRef.current += 1;
console.log(countRef.current);
}, 1000);
return () => clearInterval(timerId);
}, []);
return <div>Timer Component</div>;
};
5. 考虑使用 requestAnimationFrame
对于需要频繁更新 UI 或进行动画的任务,使用 requestAnimationFrame
可能更合适。它会在浏览器下一次重绘之前调用回调函数,从而提供更流畅的动画效果。
示例:
import React, { useEffect, useRef } from 'react';
const AnimationComponent: React.FC = () => {
const requestIdRef = useRef<number>(0);
const animate = () => {
console.log('Animation frame');
requestIdRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestIdRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(requestIdRef.current); // 清理动画帧
}, []);
return <div>Animation Component</div>;
};
6. 注意组件的生命周期
确保定时器不会在组件的生命周期中导致问题,例如在组件卸载时定时器还在运行,或者在组件重新渲染时创建了重复的定时器。
当然!确保定时器在组件的生命周期内不会导致问题是很重要的。以下是一些代码示例,展示了如何在 React 组件中正确使用和清理定时器,以避免在组件卸载时定时器仍在运行,或者在组件重新渲染时创建重复的定时器。
示例 1: 使用 useEffect
清理定时器
在 useEffect
中设置定时器,并在组件卸载时清理定时器:
import React, { useEffect, useState } from 'react';
const TimerComponent: React.FC = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// 创建定时器
const timerId = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 清理定时器
return () => {
clearInterval(timerId);
};
}, []); // 依赖数组为空,确保定时器只在组件挂载时创建一次
return <div>Count: {count}</div>;
};
示例 2: 避免在重新渲染时创建重复的定时器
在 useEffect
中管理定时器,确保每次重新渲染时不会创建新的定时器:
import React, { useEffect, useRef, useState } from 'react';
const TimerComponent: React.FC = () => {
const [count, setCount] = useState(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// 创建定时器
timerRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 清理定时器
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, []); // 依赖数组为空,确保定时器只在组件挂载时创建一次
return <div>Count: {count}</div>;
};
示例 3: 动态依赖的定时器
如果定时器的行为依赖于组件的 props 或 state,可以在 useEffect
中添加依赖,并确保每次更新时清理旧的定时器:
import React, { useEffect, useRef, useState } from 'react';
const TimerComponent: React.FC<{ delay: number }> = ({ delay }) => {
const [count, setCount] = useState(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
// 清理上一个定时器(如果存在)
if (timerRef.current) {
clearInterval(timerRef.current);
}
// 创建新的定时器
timerRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, delay);
// 清理定时器
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
}, [delay]); // 当 delay 改变时重新创建定时器
return <div>Count: {count}</div>;
};
这些实践有助于避免在组件卸载时定时器仍在运行,或在组件重新渲染时创建重复的定时器,从而确保组件行为的正确性和性能。
7. 避免阻塞主线程
避免在定时器中执行耗时的操作,这样可以防止阻塞主线程,导致应用响应变慢。
总结
在前端项目中使用 setTimeout
和 setInterval
时,要注意清理定时器、避免过多使用、处理定时器的延迟和准确性、避免访问过期的状态、选择合适的定时方法(如 requestAnimationFrame
)、考虑组件的生命周期,以及避免阻塞主线程。通过遵循这些实践,可以提高代码的可靠性和性能。
三、页面进入后台和重新回到前台场景
有个更进一步的问题,在页面进入后台或者重新回到前台,定时器的工作机制?
当页面进入后台(即不可见)或重新回到前台(即可见)时,定时器的工作机制可能会受到浏览器的影响。
浏览器通常会对不可见的页面进行性能优化,以减少资源消耗,这会影响 setTimeout
和 setInterval
的行为。
以下是一些关键点和建议,以确保定时器在这些情况下能够正确工作:
1. 浏览器优化
-
页面不可见时的性能优化: 许多现代浏览器会在页面不可见时暂停定时器(例如,
setInterval
)或降低其调用频率。这是为了节省资源和电池寿命。 -
文档可见性 API: 使用
document.visibilityState
和visibilitychange
事件来检测页面是否可见,并根据需要调整定时器的行为。
示例:
import React, { useEffect, useRef, useState } from 'react';
const TimerComponent: React.FC = () => {
const [count, setCount] = useState(0);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// 页面重新变为可见,重新启动定时器
if (!timerRef.current) {
timerRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
}
} else {
// 页面变为不可见,清理定时器
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
}
};
useEffect(() => {
// 设置定时器
timerRef.current = setInterval(() => {
setCount(prevCount => prevCount + 1);
}, 1000);
// 监听可见性变化
document.addEventListener('visibilitychange', handleVisibilityChange);
// 清理
return () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return <div>Count: {count}</div>;
};
2. 使用 requestAnimationFrame
requestAnimationFrame
会在页面的下一次重绘前调用回调函数,并且当页面不可见时,浏览器会暂停 requestAnimationFrame
调用。这对于动画任务是有利的,但如果需要持续运行的定时任务,可能需要结合其他机制。
示例:
import React, { useEffect, useRef, useState } from 'react';
const AnimationComponent: React.FC = () => {
const [count, setCount] = useState(0);
const requestIdRef = useRef<number>(0);
const animate = () => {
setCount(prevCount => prevCount + 1);
requestIdRef.current = requestAnimationFrame(animate);
};
useEffect(() => {
requestIdRef.current = requestAnimationFrame(animate);
// 页面不可见时停止动画
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
requestIdRef.current = requestAnimationFrame(animate);
} else {
cancelAnimationFrame(requestIdRef.current);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
cancelAnimationFrame(requestIdRef.current);
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
return <div>Count: {count}</div>;
};
总结
- 浏览器优化: 现代浏览器会对不可见页面的定时器进行优化。使用
document.visibilityState
和visibilitychange
事件可以帮助你处理这些情况。 requestAnimationFrame
: 对于动画任务,requestAnimationFrame
是更合适的选择,但也会受到浏览器优化的影响。
这些实践可以帮助你在前端项目中处理定时器,确保它们在页面可见和不可见状态下的行为符合预期。
那假如我们希望在后台仍然执行定时任务呢?
- stackoverflow - How can I make setInterval also work when a tab is inactive in Chrome?
- reddit - setInterval not working in the inactive tab
这里有两个相关话题的讨论,可以阅读和参考。