前言

今天是学习react的第五天啦,我的第一个小目标是将平时使用的todo清单软件通过react在web端一比一的实现所有功能!

前面写的文章都会放在前言中:

📦代码仓库链接 react-todo gitee仓库

💻在线预览效果 react-todo 开发进度

# 👀从零开始学React第一天~React基础框架的构建(Create React App+Tailwind css+Material ui)

# 👀从零开始学React第二天~React配置Eslint+路由导航的实现(react-router-dom)

# 👀从零开始学React第三天~React日期选择器组件开发+Dayjs的使用

# 👀从零开始学React第四天~📆实现一个好看的弹窗日历组件

今日任务

今天不赶开发进度,将前面写的代码进行一下优化,深入理解一下 React hook 的运行机制,磨刀不误砍柴工嘛~

当然性能优化的前提是保障功能完好的情况下减少不必要的损耗,过度优化不可取~

今天的文章开始前安利一下 react新文档链接,整体的文档架构更加简单清晰,还多了很多的案例。感谢 @半边醉月将影 的推荐~

开始优化

首先我们先打印一下组件的渲染情况,我分别给layout,DatePicker时间选择器组件DatePopover弹窗组件Calendar日历组件这几个相互嵌套的组件的函数渲染时打印一下。

优化首次进入重复渲染

首次进入页面控制台结果如下:

image.png

可以看到,进入页面正常情况应该是全部组件都渲染一次,但是现在的话是各渲染了两次,那么我们就开始排查问题。

DatePicker时间选择器组件代码如下:

// DatePicker.jsx

 // ...more
export default function DatePicker() {
  console.log("DatePicker开始渲染")

  // 当前选中的日期
  const [activeDate, setDate] = useState(dayjs())
  useEffect(() => {
    setWeek(getThisWeek())
  }, [activeDate])

  // 本周七天的日期对象数组
  const getThisWeek = () => {
    return Array.from({ length: 7 }).map((item, index) => {
      return activeDate.isoWeekday(index + 1)
    })
  }

  const [thisWeek, setWeek] = useState(getThisWeek())

  // 判断是否为选择的日期
  const isActive = (item) => item.date() === activeDate.date()

  const toToday = () => {
    // 跳转至今天
    setDate(dayjs())
    setWeek(getThisWeek())
  }

  const toLastWeek = () => {
    // 显示上一周日期
    const lastWeek = thisWeek.map((item, index) => {
      return dayjs(item).isoWeekday(index - 6)
    })
    setDate(dayjs(activeDate).subtract(7, "d"))
    setWeek(lastWeek)
  }

  const toNextWeek = () => {
    // 显示下一周日期
    const nextWeek = thisWeek.map((item, index) => {
      return dayjs(item).isoWeekday(index + 8)
    })
    setDate(dayjs(activeDate).add(7, "d"))
    setWeek(nextWeek)
  }

  const [anchorEl, setAnchorEl] = useState(null)
  const showDatePopover = () => {
    const datePickerTarget = document.getElementById("date-picker")
    setAnchorEl(datePickerTarget)
  }
  return (
    <div className="p-5 flex items-center ">
      // ...more
    </div>
  )
}

首先要查找导致两次渲染的原因,在 DatePicker时间选择器组件 中有有两个通过useState定义的变量activeDatethisWeek ,当这两个变量其中一个改变的时候 组件就会重新渲染,那么有没有可能是它们导致的呢。

要触发这两个变量变化,肯定是通过 useState 时返回的方法来修改的,而后面的代码并没有直接执行这两个方法,那么问题应该是出在 useEffect 上,我在useEffect中加入一个打印查看一下效果

useEffect(() => {
    console.log("activeDate变化")
    setWeek(getThisWeek())
  }, [activeDate])

打印结果:

image.png

果然初次执行的时候就触发了useEffect的回调,而useEffect又触发了 setWeek(getThisWeek()) 导致了组件重新渲染。

这是因为getThisWeek是一个函数,每一次执行时会根据新的 activeDate 获取新的一周日期,因为在dayjs生成的日期对象中是精确到毫秒的,所以即使是同一周每一次执行返回的值也不同,我们要做的就是将getThisWeek直接变成一个变量,这样就不会触发setWeek导致组件刷新了。

//old
const getThisWeek = () => {
    return Array.from({ length: 7 }).map((item, index) => {
      return activeDate.isoWeekday(index + 1)
    })
  }

 // new
  const getThisWeek = Array.from({ length: 7 }).map((item, index) => {
    return activeDate.isoWeekday(index + 1)
  })

我们再看一下控制台,如下图:

image.png

优化点击日期重复渲染

OK这一步搞定之后,我们看一下选中一个新日期的打印结果,如下图
temp.gif

我每次点击一个日期都会使组件刷新两次,那这一点要如何解决呢?

{thisWeek.map((item) => {
  return (
    <div
      className={`flex items-center justify-center cursor-pointer w-7 h-7 rounded-full mx-1 ${
        isActive(item)
          ? "bg-primary"
          : item.isToday()
          ? "bg-gray-200"
          : "hover:bg-gray-200"
      }`}
      key={item}
      size="small"
      onClick={() => {
        setDate(item)
      }}
    >
      <span className="text-sm">
        {item.isToday() ? "今" : item.date()}
      </span>
    </div>
  )
})}

还是分析一下代码,当触发点击事件时我们执行了setDate操作,那么重新渲染一次组件肯定是没问题的,而activeDate 变量的变更又触发了useEffect 的回调函数 setWeek(getThisWeek()),因此又重新渲染了一次组件。当然这一步是没问题的,只是说如果我选择的日期还是本周的话那就没有必要去执setWeek(getThisWeek())创建一个新的星期数组了。

所以我们修改一下useEffect的第二个参数,useEffect 的第二个参数是指定数组中的变量变化后才会执行回调函数,那么我们将原本的activeDate改为activeDate.isoWeek(),每一次触发 useEffect时判断当前选中日期修改后和修改前是否为同一周,如果是同一周就不需要触发回调。

// old
useEffect(() => {
    setWeek(getThisWeek)
  }, [activeDate])
  
//new
useEffect(() => {
    setWeek(getThisWeek)
  }, [activeDate.day()])

我们看一下控制台的打印结果,可以看到每次选中日期只渲染一次啦,如下图:

temp.gif

优化日历重复渲染

我们先看看展开日历时的打印,如下图:

temp.gif

可以看到,展开是没有什么问题的,父组件重新渲染展开了日历,随之日历也执行渲染,但是如果我们关闭日历时呢?
如下图:

temp.gif
在关闭日历时,日历又执行了一次渲染,这一步的话就属于重复渲染了,我已经关闭它了不需要它再更新数据。日历之所以会在关闭时又执行一次渲染是因为根据react的组件渲染机制父组件重新渲染时子组件也会随着重新渲染

如果要避免这种情况要怎么办呢?我们可以使用 React.memo 来解决这个问题,官方的解释如下

如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useStateuseReduceruseContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。

简而言之使用了React.memo 包裹的子组件函数只有在接受到的参数变化时,子组件才重新渲染,如果接受到的参数没变化即使父组件刷新也不会导致重新渲染啦~

具体的实现也很简单,核心代码如下:

import React, { useState } from "react"
function Calendar(props) {
    // ...
}
export default memo(Calendar)

memo 将Calendar组件给包裹起来再暴露出去即可~我们看一下修改后的控制台输出

temp.gif

这次关闭日历时就不会额外渲染一次啦~

优化跳转今日按钮

最后我们的右边还有一个小太阳按钮,如果点击小太阳就会执行toToday方法跳转到今天,但是如果当前选中的日期就是今天的话还是会重复渲染一次,如下图

temp.gif

这一步也简单啦,修改一下toToday方法加一个判断即可,我使用了dayjs提供的isSame方法来判断,记得传入第二个参数将颗粒度设置为日,代码如下

const toToday = () => {
    // 跳转至今天
    if (dayjs().isSame(activeDate,"day")) {
      setDate(dayjs())
      setWeek(getThisWeek)
    }
  }

总结

由于目前代码量还比较少,可优化的点还比较少,也就是我这种第一次写hook能找出这么多可优化的地方了😭

这一次没有使用到 useMemouseCallback 这两个hook,之后的话代码量大了应该会深入了解一下这两个方法的用途。

react相比vue来说将更多的 性能的取舍 通过api交给了开发者,对于开发者的框架熟练度和开发能力要求更高,很多的坑都需要开发经验的累积才能感觉游刃有余~

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐