前言
今天是学习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 hook 的运行机制,磨刀不误砍柴工嘛~
当然性能优化的前提是保障功能完好的情况下减少不必要的损耗,过度优化不可取~
今天的文章开始前安利一下 react新文档链接,整体的文档架构更加简单清晰,还多了很多的案例。感谢 @半边醉月将影 的推荐~
开始优化
首先我们先打印一下组件的渲染情况,我分别给layout,DatePicker时间选择器组件,DatePopover弹窗组件,Calendar日历组件这几个相互嵌套的组件的函数渲染时打印一下。
优化首次进入重复渲染
首次进入页面控制台结果如下:
可以看到,进入页面正常情况应该是全部组件都渲染一次,但是现在的话是各渲染了两次,那么我们就开始排查问题。
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
定义的变量activeDate
和 thisWeek
,当这两个变量其中一个改变的时候 组件就会重新渲染,那么有没有可能是它们导致的呢。
要触发这两个变量变化,肯定是通过 useState 时返回的方法来修改的,而后面的代码并没有直接执行这两个方法,那么问题应该是出在 useEffect 上,我在useEffect
中加入一个打印查看一下效果
useEffect(() => {
console.log("activeDate变化")
setWeek(getThisWeek())
}, [activeDate])
打印结果:
果然初次执行的时候就触发了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)
})
我们再看一下控制台,如下图:
优化点击日期重复渲染
OK这一步搞定之后,我们看一下选中一个新日期的打印结果,如下图
我每次点击一个日期都会使组件刷新两次,那这一点要如何解决呢?
{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()])
我们看一下控制台的打印结果,可以看到每次选中日期只渲染一次啦,如下图:
优化日历重复渲染
我们先看看展开日历时的打印,如下图:
可以看到,展开是没有什么问题的,父组件重新渲染展开了日历,随之日历也执行渲染,但是如果我们关闭日历时呢?
如下图:
在关闭日历时,日历又执行了一次渲染,这一步的话就属于重复渲染了,我已经关闭它了不需要它再更新数据。日历之所以会在关闭时又执行一次渲染是因为根据react的组件渲染机制,父组件重新渲染时子组件也会随着重新渲染。
如果要避免这种情况要怎么办呢?我们可以使用 React.memo 来解决这个问题,官方的解释如下
如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在
React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅检查 props 变更。如果函数组件被React.memo
包裹,且其实现中拥有useState
,useReducer
或useContext
的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
简而言之使用了React.memo 包裹的子组件函数只有在接受到的参数变化时,子组件才重新渲染,如果接受到的参数没变化即使父组件刷新也不会导致重新渲染啦~
具体的实现也很简单,核心代码如下:
import React, { useState } from "react"
function Calendar(props) {
// ...
}
export default memo(Calendar)
用 memo 将Calendar组件给包裹起来再暴露出去即可~我们看一下修改后的控制台输出
这次关闭日历时就不会额外渲染一次啦~
优化跳转今日按钮
最后我们的右边还有一个小太阳按钮,如果点击小太阳就会执行toToday
方法跳转到今天,但是如果当前选中的日期就是今天的话还是会重复渲染一次,如下图
这一步也简单啦,修改一下toToday
方法加一个判断即可,我使用了dayjs提供的isSame
方法来判断,记得传入第二个参数将颗粒度设置为日,代码如下
const toToday = () => {
// 跳转至今天
if (dayjs().isSame(activeDate,"day")) {
setDate(dayjs())
setWeek(getThisWeek)
}
}
总结
由于目前代码量还比较少,可优化的点还比较少,也就是我这种第一次写hook能找出这么多可优化的地方了😭
这一次没有使用到 useMemo
和 useCallback
这两个hook,之后的话代码量大了应该会深入了解一下这两个方法的用途。
react相比vue来说将更多的 性能的取舍 通过api交给了开发者,对于开发者的框架熟练度和开发能力要求更高,很多的坑都需要开发经验的累积才能感觉游刃有余~