为什么我的地图中的日期变量会重置

Why does my date variable reset in my map

我正在关注这个 tutorial 制作一个 javascript 日历并尝试在 React

中实现它

工作 javascript 版本在此 jsfiddle

import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')

const Home: NextPage = () => {
  const today = new Date()
  const [currentMonth, setCurrentMonth] = useState(today.getMonth())
  const [currentYear, setCurrentYear] = useState(today.getFullYear())
  const calendarBodyRef = useRef<HTMLDivElement>(null)

  const rows = 6
  const cells = 7
  const firstDay = (new Date(currentYear, currentMonth)).getDay()

  // check how many days in a month code from https://dzone.com/articles/determining-number-days-month
  const daysInMonth = (iMonth: number, iYear: number) => {
    return 32 - new Date(iYear, iMonth, 32).getDate()
  }

  return (
    <>
      <Head>
        <title>Calendar Budget App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.calendarWrap}>
        <h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
        <div className={styles.daysWrap}>
          <span>Sun</span>
          <span>Mon</span>
          <span>Tue</span>
          <span>Wed</span>
          <span>Thu</span>
          <span>Fri</span>
          <span>Sat</span>
        </div>
        <div ref={calendarBodyRef} className={styles.calendarBody}>
          {[...Array(rows).keys()].map((row) => {
            let date = 1
            return (
              <div key={row} className={styles.row}>
                {[...Array(cells).keys()].map((cell) => {
                  if (row === 0 && cell < firstDay) {
                    return (
                      <div key={cell} className={styles.cell}></div>
                    )
                  } else if (date > daysInMonth(currentMonth, currentYear)) {
                    return
                  } else {
                    const cellText = String(date)
                    date++
                    return (
                      <div key={cell} className={styles.cell}>{cellText}</div>
                    )
                  }
                })}
              </div>
            )
          })}
        </div>
      </div>
    </>
  )
}

export default Home

但是,在 else 语句中的 date 变量递增后,我在每一行的末尾将我的计数重置回 1

如何停止在每行末尾将我的 date 变量重置回 1

简单的回答,将您的打印提取到另一个函数中并使用您的组件的 return 语句调用它。 还要注意你的打印语句是 O(n^2) 所以 re-rendering 是一个昂贵的操作。

代码:

import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')

const Home: NextPage = () => {
  const today = new Date()
  const [currentMonth, setCurrentMonth] = useState(today.getMonth())
  const [currentYear, setCurrentYear] = useState(today.getFullYear())
  const calendarBodyRef = useRef<HTMLDivElement>(null)

  const rows = 6
  const cells = 7
  const firstDay = (new Date(currentYear, currentMonth)).getDay()

  // check how many days in a month code from https://dzone.com/articles/determining-number-days-month
  const daysInMonth = (iMonth: number, iYear: number) => {
    return 32 - new Date(iYear, iMonth, 32).getDate()
  }

  const renderCalendar = () => {
            let date = 1;
            return [...Array(rows).keys()].map((row) => {
            return (
              <div key={row} className={styles.row}>
                {[...Array(cells).keys()].map((cell) => {
                  if (row === 0 && cell < firstDay) {
                    return (
                      <div key={cell} className={styles.cell}></div>
                    )
                  } else if (date > daysInMonth(currentMonth, currentYear)) {
                    return
                  } else {
                    const cellText = String(date)
                    date++
                    return (
                      <div key={cell} className={styles.cell}>{cellText}</div>
                    )
                  }
                })}
 </div>
            )
          })}
  }

  return (
    <>
      <Head>
        <title>Calendar Budget App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.calendarWrap}>
        <h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
        <div className={styles.daysWrap}>
          <span>Sun</span>
          <span>Mon</span>
          <span>Tue</span>
          <span>Wed</span>
          <span>Thu</span>
          <span>Fri</span>
          <span>Sat</span>
        </div>
        <div ref={calendarBodyRef} className={styles.calendarBody}>
          {renderCalendar()}
        </div>
      </div>
    </>
  )
}

export default Home

我喜欢 michmich 112 的回答,但遗憾的是我无法对其发表评论(缺乏声誉)

但是您可以通过添加 useMemo

来缩短渲染时间
import { useState, useRef, useMemo } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.scss'

const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
// console.log('render')

const Home: NextPage = () => {
  const today = new Date()
  const [currentMonth, setCurrentMonth] = useState(today.getMonth())
  const [currentYear, setCurrentYear] = useState(today.getFullYear())
  const calendarBodyRef = useRef<HTMLDivElement>(null)

  const rows = 6
  const cells = 7
  const firstDay = (new Date(currentYear, currentMonth)).getDay()

  // check how many days in a month code from https://dzone.com/articles/determining-number-days-month
  const daysInMonth = (iMonth: number, iYear: number) => {
    return 32 - new Date(iYear, iMonth, 32).getDate()
  }

  const renderCalendar = useMemo(() => {
            let date = 1;
            return [...Array(rows).keys()].map((row) => {
            return (
              <div key={row} className={styles.row}>
                {[...Array(cells).keys()].map((cell) => {
                  if (row === 0 && cell < firstDay) {
                    return (
                      <div key={cell} className={styles.cell}></div>
                    )
                  } else if (date > daysInMonth(currentMonth, currentYear)) {
                    return
                  } else {
                    const cellText = String(date)
                    date++
                    return (
                      <div key={cell} className={styles.cell}>{cellText}</div>
                    )
                  }
                })}
 </div>
            )
          })}
  }, [currentMonth, currentYear])

  return (
    <>
      <Head>
        <title>Calendar Budget App</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.calendarWrap}>
        <h2 className={styles.monthTitle}>{months[currentMonth]} {currentYear}</h2>
        <div className={styles.daysWrap}>
          <span>Sun</span>
          <span>Mon</span>
          <span>Tue</span>
          <span>Wed</span>
          <span>Thu</span>
          <span>Fri</span>
          <span>Sat</span>
        </div>
        <div ref={calendarBodyRef} className={styles.calendarBody}>
          {renderCalendar()}
        </div>
      </div>
    </>
  )
}

export default Home

我对接受的答案及其讨论不太满意,所以这里有一个相当长的答案来提供一些额外的上下文。我希望它可能会有所帮助,如果没有,那我想那是运气不好。


从逻辑上讲,您的问题是您在 [...Array(rows).keys()].map 回调 (let date = 1) 中初始化变量。将初始化移到循环外将解决这个逻辑问题。

从语法上讲,您不能将它移到 map 调用之外,因为您正在使用 JSX,which just provides syntactic sugar 用于 React.createElement(component, props, ...children) 调用。当您的 map 调用 returns 一个 JSX 元素数组,并因此成为 children 的有效参数时,在其位置使用 let 的变量声明将导致语法错误。

如其他答案所示,将渲染逻辑提取到一个单独的函数以便将初始化移动到循环之前,可以解决此问题。然而,重要的是,它 不提取相同的逻辑 但允许您稍微更改逻辑(通过将初始化移到循环之外)。此外,新功能并不是绝对必要的,其他 语法 hacks 可以让你达到同样的目的,例如:

<div key={row} className={styles.row}>
  {[0].map((e) => {
    let date = 1;
    return [...Array(cells).keys()].map((cell) => {
      if (row === 0 && cell < firstDay) {
        return <div key={cell} className={styles.cell}></div>;
      } else if (date > daysInMonth(currentMonth, currentYear)) {
        return;
      } else {
        const cellText = String(date);
        date++;
        return (
          <div key={cell} className={styles.cell}>
            {cellText}
          </div>
        );
      }
    });
  })}
</div>;

我相信你的情况,你甚至可以将初始化移动到组件的顶部。


您最终遇到的情况是具有副作用的回调函数之一(在回调外部声明的 date 从回调内部更改),这通常会对代码的可读性和可维护性产生负面影响。 JSX 与声明式方法和 Functional Programming 配合得很好(正如您对 map 的使用所示)。

下面是一个示例,说明您如何通过计算无副作用的天数来实现您的目标。

要计算在每个单元格中显示的数字,它依赖于单元格索引和特定月份偏移量中的因素。用于计算的索引由 map 作为 callback function 的第二个参数提供。一步一步:

  1. 它计算当前周之前处理的天数,从当前派生weekIndex

  2. 它加上本周处理的天数,从当前 dayIndex

  3. 它添加 +1 以达到基于 1 的单元格索引。这是必需的,因为 dayIndex(与 JavaScript 中的所有索引一样)是基于 0 的。

  4. 只有当月的第一天是星期日 (offset = 0) 时,基于 1 的单元格索引才是正确的天数。因此,本月的offset计算因素。

结果是一个简洁的函数,计算成本低:

const calculateDay = (weekIndex, dayIndex, offset) =>
        7 * weekIndex + dayIndex + 1 - offset;

// Friday April 1 : 7 * 0 + 5 + 1 - 5 = 1
// Monday April 11: 7 * 2 + 1 + 1 - 5 = 11
// Sunday May 1   : 7 * 0 + 0 + 1 - 0 = 1
// Tuesday May 31 : 7 * 4 + 2 + 1 - 0 = 31

const {
  useState
} = React;

const App = () => {
  const today = new Date();
  const [currentMonth, setCurrentMonth] = useState(today.getMonth());
  const currentYear = today.getFullYear();
  
  const months = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec"
  ];
  const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

  const daysInMonth = (iMonth, iYear) => {
    return 32 - new Date(iYear, iMonth, 32).getDate();
  };

  const calculateDay = (weekIndex, dayIndex, offset) =>
    7 * weekIndex + dayIndex + 1 - offset;

  const renderCalendar = () => {
    const totalDays = daysInMonth(currentMonth, currentYear);
    const firstDayInMonth = new Date(currentYear, currentMonth).getDay();
    const totalWeeks = Math.ceil((firstDayInMonth+totalDays) / 7);

    return (
      <div className="table">
        <div className="row">
          {weekDays.map((e) => (
            <div className="column" key={`col${e}`}>
              {e}
            </div>
          ))}
        </div>
        {[...Array(totalWeeks)].map((w, wIx) => {
          return (
            <div className="row">
              {weekDays.map((d, dIx) => (
                <div className="column">
                  {(wIx === 0 && dIx < firstDayInMonth) ||
                  calculateDay(wIx, dIx, firstDayInMonth) > totalDays
                    ? ""
                    : calculateDay(wIx, dIx, firstDayInMonth)}
                </div>
              ))}
            </div>
          );
        })}
      </div>
    );
  };

  return (
    <div>
      <h1>Calendar Budget App</h1>
      <div>
        <select
          value={currentMonth}
          onChange={(e) => setCurrentMonth(e.target.value)}
        >
          {months.map((m, i) => (
            <option value={i}>{m}</option>
          ))}
        </select>
        <h2>
          {months[currentMonth]} {currentYear}
        </h2>
        {renderCalendar()}
      </div>
    </div>
  );
};

// Render it
ReactDOM.render( <App / > ,
  document.getElementById("root")
);
.App {
  font-family: sans-serif;
  text-align: center;
}

.table {
  margin: 48px 0;
  box-shadow: 0 5px 10px -2px #cfcfcf;
  font-family: Arial;
  display: table;
  width: 100%;
}

.row {
  display: table-row;
  min-height: 48px;
  border-bottom: 1px solid #f1f1f1;
}

.column {
  display: table-cell;
  border: 1px solid #f1f1f1;
  vertical-align: middle;
  padding: 10px 0;
}

.row:first-child {
  font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>