React时钟组件重渲染优化:告别频繁更新
2025-01-21 17:41:36
React 组件时钟的重渲染问题
组件频繁重渲染会导致性能下降,并且在某些情况下,还会引发其他问题,比如示例代码中TooltipComp
无法正常显示。该示例展示了一个每秒钟都在重新渲染的时钟组件,其期望的目标是每分钟更新一次。接下来,我们将分析问题的原因,并提供多种解决思路。
问题分析
观察代码可知,Clock
组件使用 useState
钩子维护 Time
状态。 useEffect
钩子里的逻辑旨在等待当前时间的秒数为 0,并将最新的时间设置到 Time
状态。组件会基于这个状态渲染当前的时、分信息,问题就出在useEffect
的等待和循环上。这段逻辑实际上会导致每秒都创建一个 interval
, 并且不停调用 setTime(now)
更新组件状态,触发组件的重渲染,因此导致了时钟组件每秒更新一次。每次更新,TooltipComp
也都会随之刷新。
解决方案
核心目标是只在分钟发生变化时才更新组件状态,这样可以减少不必要的重渲染。下面提供三种解决方式。
解决方案一:仅在分钟变化时更新
这是解决问题的直接方案。使用 setInterval
定时器,检查当前分钟是否发生改变。如果发生变化,才更新 Time
状态,否则不更新。
代码示例:
import React, { useEffect, useState } from 'react';
import TooltipComp from '@components/Shared/TooltipComp';
import { useTranslation } from 'react-i18next';
const Clock = () => {
const [Time, setTime] = useState(new Date());
const { t } = useTranslation();
const formatTime = (time) => {
return time < 10 ? `0${time}` : time
}
const date = Time.toLocaleDateString(t('locale'), {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
useEffect(() => {
let intervalId = setInterval(() => {
const now = new Date();
if (now.getMinutes() !== Time.getMinutes()) {
setTime(now);
}
}, 1000);
return () => clearInterval(intervalId);
}, [Time]);
return (
<>
<TooltipComp text={date}>
<div style={{fontSize: "9px", fontFamily: "PX", cursor: "default"}}>
{formatTime(Time.getHours())}
 : 
{formatTime(Time.getMinutes())}
</div>
</TooltipComp>
</>
);
};
export default Clock;
操作步骤:
- 替换原有
useEffect
实现为以上代码。 - 注意
useEffect
依赖数组增加了Time
,以便组件更新的时候重新计算计时器,防止缓存。 - 测试是否能够每分钟更新一次时间。
解释:
setInterval
每秒运行一次,当分钟数发生改变时,通过调用setTime(now)
更新Time
状态,这将导致组件重渲染。 否则不会更新状态,也不会引起重渲染。这种方案精简,代码逻辑更加易读。
解决方案二:使用 useRef
和定时器
另一种方法使用 useRef
存储一个prevMinute
的值,每次判断是否更新。
代码示例:
import React, { useEffect, useState, useRef } from 'react';
import TooltipComp from '@components/Shared/TooltipComp';
import { useTranslation } from 'react-i18next';
const Clock = () => {
const [Time, setTime] = useState(new Date());
const { t } = useTranslation();
const prevMinute = useRef(Time.getMinutes());
const formatTime = (time) => {
return time < 10 ? `0${time}` : time
}
const date = Time.toLocaleDateString(t('locale'), {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})
useEffect(() => {
const intervalId = setInterval(() => {
const now = new Date();
if (now.getMinutes() !== prevMinute.current) {
setTime(now);
prevMinute.current = now.getMinutes();
}
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<>
<TooltipComp text={date}>
<div style={{fontSize: "9px", fontFamily: "PX", cursor: "default"}}>
{formatTime(Time.getHours())}
 : 
{formatTime(Time.getMinutes())}
</div>
</TooltipComp>
</>
);
};
export default Clock;
操作步骤:
- 引入
useRef
钩子。 - 在
useEffect
钩子中初始化prevMinute
为Time
的初始分钟。 - 在计时器回调中判断是否需要更新状态,同时更新
prevMinute.current
值。 - 替换原有
useEffect
为以上代码,并观察时钟的行为。
解释:
useRef
钩子允许跨渲染存储可变值。prevMinute.current
存储了上一次的分钟值。只有当新分钟和存储值不同时才执行更新,并且prevMinute.current
更新为新分钟值。这种方案可以有效阻止状态的频繁更新。值得注意是这里useEffect
的依赖是空的。因为这里的更新状态由定时器逻辑判断,依赖改变不应该重新加载定时器,否则也会导致不断重渲染。
解决方案三: 使用 requestAnimationFrame
这种方案可以利用浏览器的优化机制,在浏览器每一帧渲染之前执行回调函数。
代码示例:
import React, { useState, useEffect, useRef } from 'react';
import TooltipComp from '@components/Shared/TooltipComp';
import { useTranslation } from 'react-i18next';
const Clock = () => {
const [Time, setTime] = useState(new Date());
const { t } = useTranslation();
const prevMinute = useRef(Time.getMinutes());
const formatTime = (time) => {
return time < 10 ? `0${time}` : time;
};
const date = Time.toLocaleDateString(t('locale'), {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
useEffect(() => {
let animationFrameId;
const updateTime = () => {
const now = new Date();
if(now.getMinutes() !== prevMinute.current)
{
setTime(now);
prevMinute.current = now.getMinutes();
}
animationFrameId = requestAnimationFrame(updateTime);
}
animationFrameId = requestAnimationFrame(updateTime);
return () => cancelAnimationFrame(animationFrameId);
}, []);
return (
<>
<TooltipComp text={date}>
<div style={{fontSize: "9px", fontFamily: "PX", cursor: "default"}}>
{formatTime(Time.getHours())}
 : 
{formatTime(Time.getMinutes())}
</div>
</TooltipComp>
</>
);
};
export default Clock;
操作步骤:
- 使用
requestAnimationFrame
取代setInterval
定时器。 - 在
requestAnimationFrame
回调中判断是否更新。 - 替换原
useEffect
中的代码,测试观察是否符合预期。
解释:
requestAnimationFrame
与 setInterval
的区别在于 requestAnimationFrame
是在浏览器准备好下一次重绘时才执行,可以带来更高的性能,特别在移动端。同时这里还是用了 useRef 来储存prevMinute
,只有当当前分钟与存储值不同时才更新状态。
安全建议
- 避免在循环中创建新的定时器: 在第一个方案中原代码使用了循环在创建新的定时器。这容易造成内存泄漏和其他潜在问题。
- 仔细选择正确的 Hook: 理解
useState
,useEffect
,useRef
等 Hook 的特性和使用场景,才能正确运用它们解决问题。
通过上述方案,你可以有效的解决 React 组件时钟频繁重渲染问题。在实际应用中,开发者应仔细考量选择合适的方案。