首页 > web前端 > js教程 > 正文

React 函数组件日历渲染:告别 DOM 操作,拥抱状态驱动

花韻仙語
发布: 2025-07-11 23:06:02
原创
714人浏览过

react 函数组件日历渲染:告别 dom 操作,拥抱状态驱动

本教程深入探讨了 React 函数组件中日历渲染的常见问题,特别是避免直接 DOM 操作(如 document.querySelector 和 innerHTML)。我们将详细阐述如何利用 React 的核心机制——状态管理 (useState) 和副作用钩子 (useEffect)——来构建一个完全由数据驱动的、高效且易于维护的日历组件,确保其在首次加载和用户交互时都能无缝且正确地更新视图。

引言:React 中的 DOM 操作原则

React 的核心理念是声明式 UI。这意味着开发者通过描述 UI 的“状态”来构建界面,而不是直接操作 DOM 元素。React 会根据状态的变化自动、高效地更新 DOM。这种模式极大地简化了 UI 开发,并提高了应用的可维护性。

然而,当我们在 React 组件中直接使用诸如 document.querySelector、innerHTML 或 addEventListener 等原生 DOM API 时,就违背了 React 的声明式原则。这可能导致以下问题:

  • 渲染不一致性: React 维护一个虚拟 DOM 来优化更新。直接操作真实 DOM 会绕过虚拟 DOM,导致 React 无法感知这些变化,进而引发视图与内部状态不一致的问题。
  • 性能问题: React 有一套高效的协调(Reconciliation)算法来最小化 DOM 操作。直接操作 DOM 会破坏这种优化。
  • 难以调试和维护: 逻辑分散在 React 的生命周期和原生 DOM 操作中,使得问题追踪变得复杂。
  • 兼容性与可移植性差: 代码与特定 DOM 环境耦合,难以在不同平台(如 React Native)复用。

原始问题中的日历组件正是因为混合了命令式 DOM 操作与 React 组件模式,才导致了初始化渲染的困扰和后续难以维护的局面。

原问题分析与常见误区

在原始代码中,renderCalendar() 函数内部使用了 document.querySelector 来获取 DOM 元素,并使用 innerText 和 innerHTML 直接修改它们。

export const Calendar = () => {
  const currentDate = document.querySelector(".current-date") || {}; // 问题点1
  let daysTag = document.querySelector(".days") || {};               // 问题点2
  // ...
  const renderCalendar = () => {
    // ...
    currentDate.innerText = `${months[currMonth]} ${currYear}`; // 问题点3
    daysTag.innerHTML = liTag;                                  // 问题点4
  };
  renderCalendar(); // 首次调用位置
  // ...
  prevNextIcon.forEach ( icon => {
    icon.addEventListener("click", () => { // 问题点5
      // ...
      renderCalendar();
    });
  });
  // ...
  return (
    // ... JSX 结构
  );
}
登录后复制

分析原代码中 renderCalendar() 的调用问题:

  1. 在 return 语句前调用 renderCalendar():

    • 当 React 组件首次渲染时,其函数体从上到下执行。
    • 在 return 语句执行并返回 JSX 结构之前,对应的真实 DOM 元素(如 .current-date 和 .days)尚未被创建并挂载到文档中。
    • 因此,document.querySelector(".current-date") 和 document.querySelector(".days") 在此时会返回 null。尽管代码中使用了 || {} 来避免运行时错误,但 currentDate 和 daysTag 实际上是空对象,后续对它们的 innerText 或 innerHTML 赋值将无效。
    • 这就是为什么首次加载时日历无法正确显示,而刷新后又可能暂时显示的原因(因为浏览器缓存或某种时序巧合)。
  2. 在 return 语句后调用 renderCalendar():

    • JavaScript 函数的执行流程决定,一旦遇到 return 语句,函数就会立即终止并返回结果。
    • 任何在 return 语句之后的代码都将是“不可达代码”(Unreachable code),永远不会被执行。终端中收到的“Unreachable code”警告正是对此的提示。

React 最佳实践:状态驱动与声明式渲染

解决上述问题的核心在于拥抱 React 的状态驱动和声明式渲染范式。我们将通过 useState 和 useEffect 钩子来重构日历组件。

  1. useState:管理组件状态

    • 所有影响组件渲染的数据(如当前的年份、月份、日历中的日期列表、当前日期文本等)都应该存储在 useState 中。当这些状态改变时,React 会自动重新渲染组件。
  2. useEffect:处理副作用

    • useEffect 钩子用于处理组件渲染后需要执行的“副作用”,例如数据获取、订阅事件、或基于状态变化进行复杂计算。
    • 我们可以将计算日历日期列表的逻辑放入 useEffect 中。通过指定依赖项(如 currYear 和 currMonth),我们可以确保当年份或月份变化时,日历数据会重新计算。
  3. JSX:声明式渲染 UI

    • 不再使用 innerHTML 或 innerText 直接操作 DOM。
    • 而是通过 JSX 语法,将状态数据直接绑定到 UI 元素上。例如,日期列表可以通过映射一个日期数据数组来动态生成
    • 元素。

重构日历组件

我们将逐步重构 Calendar 组件,使其完全符合 React 的开发范式。

步骤一:定义状态

首先,我们需要定义组件所需的各种状态:当前的年份、月份、日历显示的日期文本,以及日历中所有日期(包括上月、本月、下月)的列表。

import React, { useState, useEffect, useCallback } from 'react';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';

export const Calendar = () => {
  // 初始化当前日期
  const today = new Date();
  const [currYear, setCurrYear] = useState(today.getFullYear());
  const [currMonth, setCurrMonth] = useState(today.getMonth()); // 0-11
  const [currentDateText, setCurrentDateText] = useState(""); // 用于显示 "May 2023"
  const [daysInMonth, setDaysInMonth] = useState([]); // 用于存储日历中的所有日期数据

  const months = [
    "January", "February", "March", "April", "May", "June",
    "July", "August", "September", "October", "November", "December"
  ];

  // ... (其他代码)
};
登录后复制

步骤二:封装计算逻辑到 useEffect

将原 renderCalendar 的逻辑重写为一个计算函数,该函数不再直接操作 DOM,而是计算出日期数据并更新状态。然后,在 useEffect 中调用这个计算函数。

  // 使用 useCallback 优化,避免每次渲染都重新创建函数
  const generateCalendarDays = useCallback(() => {
    let firstDayofMonth = new Date(currYear, currMonth, 1).getDay(); // 获取当月第一天是星期几 (0-6)
    let lastDateofMonth = new Date(currYear, currMonth + 1, 0).getDate(); // 获取当月最后一天
    let lastDayofLastMonth = new Date(currYear, currMonth, 0).getDate(); // 获取上月最后一天

    let days = [];

    // 填充上月日期
    for (let i = firstDayofMonth; i > 0; i--) {
      days.push({
        date: lastDayofLastMonth - i + 1,
        type: 'inactive',
        key: `prev-${lastDayofLastMonth - i + 1}` // 添加唯一 key
      });
    }

    // 填充当月日期
    for (let i = 1; i <= lastDateofMonth; i++) {
      let isToday = i === today.getDate() && currMonth === today.getMonth() && currYear === today.getFullYear();
      days.push({
        date: i,
        type: isToday ? 'active' : '',
        key: `curr-${i}`
      });
    }

    // 填充下月日期 (确保日历有 6 行,即 42 个格子)
    let totalDaysDisplayed = days.length;
    let remainingSlots = 42 - totalDaysDisplayed; // 假设日历固定显示 6 周
    if (remainingSlots < 0) remainingSlots = 0; // 避免负数,虽然通常不会发生

    for (let i = 1; i <= remainingSlots; i++) {
      days.push({
        date: i,
        type: 'inactive',
        key: `next-${i}`
      });
    }

    // 更新状态
    setDaysInMonth(days);
    setCurrentDateText(`${months[currMonth]} ${currYear}`);
  }, [currYear, currMonth, today]); // 依赖项:当年份或月份变化时重新计算

  // 使用 useEffect 在组件挂载或 currYear/currMonth 变化时执行日历生成
  useEffect(() => {
    generateCalendarDays();
  }, [generateCalendarDays]); // 依赖 generateCalendarDays 函数
登录后复制

步骤三:JSX 声明式渲染

现在,我们可以利用 currentDateText 和 daysInMonth 这两个状态来渲染 UI。

  return (
    <div className="auth flexContainer">
      <h1>Calendar</h1>
      <div className="wrapper">
        <header>
          <p className="current-date">{currentDateText}</p> {/* 直接绑定状态 */}
          <div className="icons">
            <ArrowBackIosIcon id="prev" className="arrow" onClick={() => {
              setCurrMonth(prevMonth => {
                let newMonth = prevMonth - 1;
                if (newMonth < 0) {
                  setCurrYear(prevYear => prevYear - 1);
                  return 11; // 11代表12月
                }
                return newMonth;
              });
            }}/>
            <ArrowForwardIosIcon id="next" className="arrow" onClick={() => {
              setCurrMonth(prevMonth => {
                let newMonth = prevMonth + 1;
                if (newMonth > 11) {
                  setCurrYear(prevYear => prevYear + 1);
                  return 0; // 0代表1月
                }
                return newMonth;
              });
            }}/>
          </div>
        </header>
        <div className="calendar">
          <ul className="weeks">
            <li>Sunday</li>
            <li>Monday</li>
            <li>Tuesday</li>
            <li>Wednesday</li>
            <li>Thursday</li>
            <li>Friday</li>
            <li>Saturday</li>
          </ul>
          <ul className="days">
            {daysInMonth.map(day => (
              <li key={day.key} className={day.type}>{day.date}</li> // 映射数组生成列表
            ))}
          </ul>
        </div>
      </div>
    </div>
  );
登录后复制

完整的重构代码示例

import React, { useState, useEffect, useCallback } from 'react';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';

export const Calendar = () => {
  // 获取当前日期,用于标记“今天”
  const today = new Date();

  // 使用 useState 管理当前显示的年份和月份
  const [currYear, setCurrYear] = useState(today.getFullYear());
  const [currMonth, setCurrMonth] = useState(today.getMonth()); // 月份从0开始 (0-11)

  // 用于显示日历
登录后复制

以上就是React 函数组件日历渲染:告别 DOM 操作,拥抱状态驱动的详细内容,更多请关注php中文网其它相关文章!

驱动精灵
驱动精灵

驱动精灵基于驱动之家十余年的专业数据积累,驱动支持度高,已经为数亿用户解决了各种电脑驱动问题、系统故障,是目前有效的驱动软件,有需要的小伙伴快来保存下载体验吧!

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号