HOW - 计时器实践和注意事项

一、前言

HOW - 前端定时器实践(含防抖、interval 模拟)中我们简单了解过 JavaScript 中的计时器以及潜在问题,包括防抖和节流的应用。那今天我们进一步来学习计时器实践和注意事项。

二、具体介绍

在前端项目中使用 setTimeoutsetInterval 执行定时任务是很常见的,但在实践中需要注意以下几个方面,以确保代码的正确性和性能:

1. 清理定时器

在组件卸载或不再需要定时器时,务必清理定时器,以避免内存泄漏和不必要的计算:

比如在 React 项目中,使用 clearTimeoutclearInterval: 在组件的 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. 处理定时器的延迟和准确性

  • setTimeoutsetInterval 的延迟不一定精确: 特别是在 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. 避免阻塞主线程

避免在定时器中执行耗时的操作,这样可以防止阻塞主线程,导致应用响应变慢。

总结

在前端项目中使用 setTimeoutsetInterval 时,要注意清理定时器、避免过多使用、处理定时器的延迟和准确性、避免访问过期的状态、选择合适的定时方法(如 requestAnimationFrame)、考虑组件的生命周期,以及避免阻塞主线程。通过遵循这些实践,可以提高代码的可靠性和性能。

三、页面进入后台和重新回到前台场景

有个更进一步的问题,在页面进入后台或者重新回到前台,定时器的工作机制?

当页面进入后台(即不可见)或重新回到前台(即可见)时,定时器的工作机制可能会受到浏览器的影响。

浏览器通常会对不可见的页面进行性能优化,以减少资源消耗,这会影响 setTimeoutsetInterval 的行为。

以下是一些关键点和建议,以确保定时器在这些情况下能够正确工作:

1. 浏览器优化

  • 页面不可见时的性能优化: 许多现代浏览器会在页面不可见时暂停定时器(例如,setInterval)或降低其调用频率。这是为了节省资源和电池寿命。

  • 文档可见性 API: 使用 document.visibilityStatevisibilitychange 事件来检测页面是否可见,并根据需要调整定时器的行为。

示例:

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.visibilityStatevisibilitychange 事件可以帮助你处理这些情况。
  • requestAnimationFrame: 对于动画任务,requestAnimationFrame 是更合适的选择,但也会受到浏览器优化的影响。

这些实践可以帮助你在前端项目中处理定时器,确保它们在页面可见和不可见状态下的行为符合预期。

那假如我们希望在后台仍然执行定时任务呢?

这里有两个相关话题的讨论,可以阅读和参考。

  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值