返回

React时钟组件重渲染优化:告别频繁更新

javascript

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())}
                &thinsp;:&thinsp;
                {formatTime(Time.getMinutes())}
            </div>
        </TooltipComp>
    </>
  );
};

export default Clock;

操作步骤:

  1. 替换原有 useEffect 实现为以上代码。
  2. 注意useEffect依赖数组增加了Time,以便组件更新的时候重新计算计时器,防止缓存。
  3. 测试是否能够每分钟更新一次时间。

解释:
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())}
                &thinsp;:&thinsp;
                {formatTime(Time.getMinutes())}
            </div>
        </TooltipComp>
    </>
    );
};

export default Clock;

操作步骤:

  1. 引入useRef钩子。
  2. useEffect钩子中初始化prevMinuteTime的初始分钟。
  3. 在计时器回调中判断是否需要更新状态,同时更新 prevMinute.current 值。
  4. 替换原有 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())}
                &thinsp;:&thinsp;
                {formatTime(Time.getMinutes())}
             </div>
         </TooltipComp>
    </>
    );
};


export default Clock;

操作步骤:

  1. 使用requestAnimationFrame取代 setInterval 定时器。
  2. requestAnimationFrame回调中判断是否更新。
  3. 替换原useEffect中的代码,测试观察是否符合预期。

解释:

requestAnimationFramesetInterval 的区别在于 requestAnimationFrame 是在浏览器准备好下一次重绘时才执行,可以带来更高的性能,特别在移动端。同时这里还是用了 useRef 来储存prevMinute,只有当当前分钟与存储值不同时才更新状态。

安全建议

  • 避免在循环中创建新的定时器: 在第一个方案中原代码使用了循环在创建新的定时器。这容易造成内存泄漏和其他潜在问题。
  • 仔细选择正确的 Hook: 理解useState, useEffect, useRef 等 Hook 的特性和使用场景,才能正确运用它们解决问题。

通过上述方案,你可以有效的解决 React 组件时钟频繁重渲染问题。在实际应用中,开发者应仔细考量选择合适的方案。