引言
React 作为现代Web开发中最为流行的前端库之一,以其简洁的组件化思想和高效的虚拟DOM机制赢得了广大开发者的心。随着应用复杂度的不断增加,如何高效管理组件的重渲染成为了开发者面临的一大挑战。不必要的重渲染不仅会消耗更多的计算资源,还会导致用户体验下降,特别是在大型应用中,这种影响尤为明显。
为了应对这一挑战,React 提供了一系列强大的工具和钩子来帮助开发者优化性能。其中,useMemo, useCallback 和 react.memo 是三个非常重要的工具,它们在不同的场景下发挥着关键作用。通过合理使用这些工具,开发者可以显著减少不必要的重渲染,提高应用的整体性能和响应速度。
本文将深入探讨这三个工具的作用、用法以及最佳实践。我们将从React的渲染机制入手,逐步展开对useMemo, useCallback 和 react.memo 的详细介绍,并通过具体的例子来说明它们在实际项目中的应用。希望通过本文,读者能够更好地理解和掌握这些工具,从而在自己的项目中实现更高效的性能优化。
useMemo
为什么使用 useMemo ?
useMemo是性能优化的重要一环。我们都知道当我们的某个数据发生变化的时候,与这个数据有关的组件都会进行重渲染,即从头到尾全部重新执行一遍,产生虚拟dom,再经由diff算法检测变化,最后修改真实dom,渲染到页面上。 它其实是很消耗性能的。 你平时用作练习的小型App可能看不出来什么,但是当操作的数据变大,当对dom的操作变多,页面更新的速度就可能没有那么快了。比如下面这个例子:
import { useState, useMemo } from 'react'
import './App.css'
const initialItems = new Array(29_999_999).fill(0).map((_, index) => {
return {
id: index,
isSelected: index === 29_999_998,
// 初始化一个数组,数组里面包裹了3千万个对象,只有第29999998个对象中的isSelected为true
};
})
function App() {
const [count, setCount] = useState(0)
const [items] = useState(initialItems);
const selectedItem = items.find((item) => item.isSelected);
// 利用数组的find方法,找到符合条件的元素
return (
<>
<div className="counts">
<h1>Count:{count}</h1>
<h1>Selected Item:{selectedItem?.id}</h1>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
</>
)
}
export default App
在这里我们写了一个计数器,还有一个selectedItem,执行这个selectedItem要遍历一个长度为3千万的数组,这是比较耗时的,接下来我们来看效果:
哎!驻波驻波!为什么一开始它一个数字一个数字加,过了一会儿就跳跃式增加了?!
其实是主播的手速太快了,这是由渲染机制和JS的执行机制导致的:
In case Y'all din't noticed(以防你们没看见),我们每次让count更新的时候,App这个组件都要重新渲染一遍,而每渲染一遍都要执行const selectedItem = items.find((item) => item.isSelected);,也就是说每一次渲染,它都要遍历3千万长度的数组。由于JS是单线程,JS的执行线程和渲染线程是不能同时进行的,而React会批量处理状态更新,当当前渲染未完成时,新的点击事件会被排队,所以:
当上一次的遍历数组没执行完毕的时候,无数的setCount就全部进入队列了,等待上一次的数组遍历完毕后,立刻!!马上!!React就把这几个setCount一起执行渲染到了页面上了。
现在我们知道问题所在了,我们可以用useMemo解决它:
import { useState, useMemo } from 'react'
import './App.css'
const initialItems = new Array(29_999_999).fill(0).map((_, index) => {
return {
id: index,
isSelected: index === 29_999_998,
// 初始化一个数组,数组里面包裹了3千万个对象,只有第29999998个对象中的isSelected为true
};
})
function App() {
const [count, setCount] = useState(0)
const [items] = useState(initialItems);
const selectedItem = useMemo(() => items.find((item) => item.isSelected), [items]);
// 利用数组的find方法,找到符合条件的元素
return (
<>
<div className="counts">
<h1>Count:{count}</h1>
<h1>Selected Item:{selectedItem?.id}</h1>
<button onClick={() => setCount(count + 1)}>Add</button>
</div>
</>
)
}
export default App
现在的效果是这样的:
如何使用 useMemo ?
useMemo 是 React 中的一个 Hook,用于优化性能。它允许你“记住”一个计算结果,从而避免在每次渲染时都进行相同的计算,特别是在依赖项没有发生变化时。
它是这么用的:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
- 第一个参数是一个函数,该函数返回你想要缓存的值(也就是你想优化的耗时操作)。
- 第二个参数是一个依赖数组(类似于
useState或useEffect中的依赖数组)。如果数组中的任何一个值发生了变化,那么useMemo会重新计算并返回新的值;否则,它将返回之前缓存的结果。
注意,useMemo也是在渲染过程中同步执行的,它属于 React 的 "渲染流程",如果它的计算时间过长,可能会导致掉帧,也就是页面UI无法及时更新。
当浏览器或应用无法在 16.7ms 内完成一帧的渲染任务时,会跳过某些帧,导致画面更新变慢,表现为掉帧。
useMemo 优化的内容是本组件的逻辑,如果要优化子组件的函数逻辑,那就得用react.memo了。
React.memo
React.memo 是 React 提供的一个高阶组件(HOC) ,用于优化函数组件的性能。它通过记忆(Memoization) 技术,避免组件在不必要的 props 变化时重新渲染,从而提升应用性能。
为什么需要 react.memo?
当父组件重新渲染时,即使子组件的 props 实际未发生变化,React 默认也会重新渲染子组件。React.memo 可以阻止这种不必要的渲染,只有在 props 真正改变时才会重新渲染子组件。
这个API主要就是优化子组件的渲染所用的,当它没有必要渲染的时候,就别渲染了。
如何使用 react.memo?
const MyComponent = React.memo(function MyComponent(props) {
// 组件逻辑
});
memo 的作用是返回一个可复用的优化组件,它会以后面的组件函数创建一个新的,可以优化的,避免不必要渲染的组件。
我们来看一个实际例子,我们建立了一个计数器(属于父组件)和一个输入框(子组件):
import React, { useState } from "react";
function Child({ text }) {
console.log("Child 渲染了!"); // 仅在 text 变化时打印
return <div>子组件接收的文本: {text}</div>;
};
// 父组件
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState("Hello");
return (
<div>
<h2>父组件计数器: {count}</h2>
<button onClick={() => setCount(count + 1)}>增加计数</button>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="输入文本"
/>
{/* Child 组件只依赖 text,不依赖 count */}
<Child text={text} />
</div>
);
}
export default Parent;
当count发生变化时,父组件会重新渲染,由于父组件重新渲染时,子组件也会重新渲染,所以会是这样:
而当我们给子组件函数加上memo后,就能阻止渲染了:
const Child = React.memo(function Child({ text }) {
console.log("Child 渲染了!");
return <div>子组件接收的文本: {text}</div>;
});
现在利用useMemo和React.memo,我们可以对本组件以及子组件进行优化了,但是还有一种情况我们没有优化:当我们给子组件传入父组件的函数时,在父组件每次渲染时都会重新创建,导致每次都会被当作新函数传入子组件,引起渲染,要解决这个问题,就需要利用useCallback了。
useCallback
useCallback 是 React 中的一个 Hook,用于优化性能,避免不必要的函数重新创建。它通过缓存(记忆)函数,在依赖项不变时返回相同的函数引用,从而减少子组件的不必要渲染。
为什么需要 useCallback?
在 React 中,每次组件重新渲染时,其内部的函数会被重新创建。如果将这些函数作为 props 传递给子组件(尤其是用 React.memo 优化的子组件),子组件会因接收到新的函数引用而触发不必要的渲染。useCallback 通过缓存函数解决这个问题。
React.memo 和 useCallback 通常是配合使用的,目的是避免子组件因父组件的无关更新(如状态变化)而触发不必要的渲染:
React.memo 会对组件的 props 进行浅比较,如果 props 未变化,则跳过子组件的渲染。
但如果父组件传递的是新创建的函数(如内联函数或未缓存的函数),每次父组件渲染时,子组件的 props 会被判定为“变化”,导致重新渲染。
useCallback 缓存函数,在依赖项不变时返回相同的函数引用。
这样,父组件重新渲染时,如果依赖项未变,子组件接收到的函数引用不变,React.memo 的浅比较会认为 props 未变化,从而跳过子组件的渲染。
如何使用 useCallback ?
const memoizedCallback = useCallback(fn, dependencies);
- fn: 需要缓存的函数。
- dependencies: 依赖项数组,只有当依赖项变化时,才会重新创建函数。
拿下面的例子来直观展示吧!
const Child = React.memo(({ onClick }) => {
console.log("Child rendered");
return <button onClick={onClick}>Click</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Button clicked");
};
return (
<>
<Child onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>Parent: {count}</button>
</>
);
}
当<Child />组件重渲染时,会触发console.log("Child rendered");
<Child />组件传入了父组件的handleClick()方法。
我们可以看到,当父组件的数据变化时,父组件重新渲染了,导致父组件的函数也重新创建,子组件检测到这不是以前传入的函数了(即使函数名函数内容都一样,他也不同了),所以子组件也重新渲染了。
我们说过渲染会耗费很大的性能,所以为了避免组件进行不必要的渲染,我们就用到了React.memo,现在我们要用useCallback来进一步优化这种触发不必要渲染的情况。
于是将const handleClick改为:
const handleClick =
useCallback(() => {
console.log("Button clicked");
}, []); // 无依赖,函数始终不变
结果就成为了:
在这里我们看到父组件的重新渲染并没有触发子组件的渲染。
总结
React是数据驱动的,数据驱动页面的变化,驱动页面的渲染,但是我们知道有的时候页面的渲染不会合我们心意,比如:
组件某个部分发生变化,导致整个组件重新渲染的时候,其他地方跟着一起渲染。
父组件发生重新渲染的时候,导致没有任何变化的子组件重新渲染。
为了解决这种无意义的渲染所造成的掉帧等性能问题,我们引入了useMemo,React.memo以及useCallback.
useMemo主要用于修改本组件的逻辑,保护本组件一些逻辑不受其他变量的变化所影响,以至于重新渲染。
React.memo和useCallback主要用于保护子组件的逻辑不受父组件的变化影响,而导致重新渲染,这两个都是配合使用的,React.memo像是把门关了90%,而配合上useCallback就把整个门全部关严实了————无论父组件如何变化,都不会导致子组件的无意义渲染,子组件只会在该渲染的时候进行渲染。