为什么要在函数组件中使用React.memo?
这里说明一下吧!如果大家对这个标题有观感上的疑问的话,不妨花些时间仔细阅读一下这篇文章.相反地呢?如果是观感上第一时间就能得出结论的情况的话,那就可以去了解其他文章的相关知识点了.
那么接下来就不废话了,直接长刀直入,进入正题!
初探memo
首先让我们用一个例子走进React.memo的世界
呆呆的函数组件 - 没有使用memo
在函数组件中,若未采用React.memo,则该组件如同一个人失去了大脑而显得迟钝而迟缓.
不信我们就来看下面的Demo
点击访问演示Demo

让我们来分析下上图发生的流程:
- 首次启动时,在页面内容中加载并呈现App组件和B组件,并将结果输出到控制台(见下图)。
- 单击按钮触发事件后会触发App组件数值变化的计算,并使所有相关数据重新计算并更新状态。
那么问题就来了,按正常逻辑来说,应该是这样的流程才对:
- 页面初次显示时,App组件与B组件各自完成更新,并记录相关信息。
- 当App组件内部的数据发生变更时,则迫使该组件重新呈现新的状态。
- 数据变化若与B组件无关联,则无需对其造成影响。
但理想过于美好而现实却相当骨感!事实则是App组件已经进行了更新,B组件也相应地进行了升级,这并不是我们所希望看到的效果,因为从B组件的角度来看:虽然我没有做出任何改变,但却要求我重新配置一遍!
无效渲染的原因
那么造成无效渲染的原因是啥呢?
从技术原理上讲,
函数组件本身不具备接收或处理prop值的能力,
当父组件发起状态更新时,
实际上等同于为子组件重置了所有的prop值。
因此,
在这种情况下,
B组件就像一个没有记忆功能的普通组件,
实际是一个完全普通的、不具有任何特殊缓存机制的元素。
由于在这种情况中,
B组件无法区分不同的prop变化,
当父组件的状态发生变化时,
在这种情况下(即未使用React.memo),
B组件会重新渲染自己以适应新的状态。
从而导致B组件在相同的输入下重复渲染。
给憨憨带上脑子 - 使用memo进行包裹
在为一个函数组件注入逻辑能力时(或:当我们为其注入逻辑能力时),就按照下面的方式进行操作。
import React form 'react';
const FuncComponent = ()=>{
return <h1>火热很火辣</h1>
}
export default React.memo(FunComponent);
代码解读
就不会发生上面那种,无脑render组件的情况了
试着把上面demo中B组件代码里最后一行的注释放开试一下吧

然后像上面一样再次点击一下按钮,看看控制台的打印结果:

Yep! 只更新了App组件,符合预期!
那么到底是为什么造成的这种原因呢
所以到这里还不算完,让我们进一步升温
激情升温 - 深入探索
到此为止我们还不太明白memo是如何实现避免无效更新的过程。接下来我们将深入探讨这一机制。
class组件中的性能优化点
可能有人注意到class组件中存在一个具有类似功能的组件,称为PureComponent;其功能与memo几乎完全相同.
来回顾一下,我们在class组件中经常用到的写法:
import React, {PureComponent} from 'react';
class Demo extends PureComponent {
// 性能优化点
shouldComponentUpdate(nextProps, nextState){
// 默认始终返回true
return true;
}
render() {
return <h1>听懂掌声</h1>
}
}
代码解读
总体而言其实Primitive和memo都是通过对props值进行轻量级比较来判断该组件是否需要更新的。
如果我们在class组件中未主动使用PureComponent,则可以通过手动决定该组件的更新状态来实现功能。
在 shouldComponentUpdate lifecycle, come to compare current props and state values, returns a boolean value (true or false) to determine if the component needs updating. Actually, the PureComponent component simplifies this comparison process by encapsulating the comparison functionality, allowing us to use it without having to manually write code. This makes it more convenient for us to utilize without needing complex setup. Furthermore, this avoids the need for manual code writing and repetitive optimization steps.
memo的功能实现
这是我提出的猜想:Memo和PureComponent本质上有着相似之处。从开发者视角出发,在实际应用中发现类组件通常具备这样的优化手段。推广钩钩(Hook)技术后,在函数组件中必然需要类似的辅助方法来减轻代码优化的工作负担。由此可见,在功能实现上两者应具有高度的一致性,并附上纯成分解对比示例供进一步理解参考
function shallowEqual (objA: mixed, objB: mixed): boolean {
// 这里的is是判断两个值是否相等,只不过是对 + 0 和 - 0,以及 NaN 和 NaN 的情况进行了特殊的处理封装,目前react源码中好像有一套新的is判断
if (is (objA, objB)) {
return true;
}
// 判断是否为对象类型
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
// 比较两个对象的,属性数量是否相等
const keysA = Object.keys (objA);
const keysB = Object.keys (objB);
if (keysA.length !== keysB.length) {
return false;
}
// 比较两个对象的的属性是否相等,值是否相等
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call (objB, keysA [i]) ||
!is (objA [keysA [i]], objB [keysA [i]])
) {
return false;
}
}
return true;
}
代码解读
在React框架中,实现元素值比较的基础代码就是该处的源码。同样决定了PureComponent及其memo对象是否需要更新组件的状态。
memo配合useMemo、useCallback
在项目优化过程中通常会将函数组件通过memo技术包裹 并搭配使用useMemo和useCallback
对于使用useMemo和useCallback的问题而言, 我无意进行冗长的大段文字阐述, 因为在该领域已存在许多高质量的文章, 大家可以通过查阅参考来获取更多信息. 我只想就其中的核心概念做个浅显易懂的概述.
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
代码解读
返回值是一个名为memoizedCallback的功能块,在传递给第一个参数时被赋值。它保留了memoization特性,并且始终指向传递给它的那个功能块——因此避免在组件更新时重新声明自身以防止内存引用地址的变化。
只有当第二个参数数组中的某些依赖发生变化时(而不是所有情况),该返回值才会维持其原有的状态。
应用场景
经常使用在父组件A向子组件B传递一个函数作为prop值的时候
父组件A:
import React,{ useCallback } form 'react';
const A = () => {
return (
// 如果不使用useCallback包裹的话,每次A的更新,都会重新声明这个handleClick的这个函数,导致B组件无效的更新
<B handleClick={ useCallback( () => //doSomething,[x,xx]) };
);
}
export default A;
代码解读
子组件B:
import React,{ memo } form 'react';
const B = (props) => {
const { handleClick } = props;
return <div onClick={ handleClick }>卑微小B在线被Diss</div>;
}
// 这里需要注意,要配合memo使用,否则的话不带脑子的B组件会始终认为传递过来的prop值都是一个全新的
export default memo(B);
代码解读
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
代码解读
其实在使用useCallback时非常相似,在这种情况下useMemo会返回一个特定类型的数据对象而不是一个独立的操作符。
curryby的第一个参数是一个子程序(subroutine),该子程序一旦被调用就会生成并立即执行另一个操作符。
只有当第二个参数数组中的依赖项发生变化时才会触发新的计算结果;一旦发生了变化就不会再保持原先的结果。
useCallback能做的事useMemo都能做,但是还是推荐各司其职
const fn = useCallback( () => //doSomething , [x,xx])
// 相当于
const fn = useMemo( ()=> () => //doSomething , [x,xx])
// 因为useMemo的返回值是第一个函数的返回值,所以只要让第一个参数的函数返回一个函数就可以达到useCallback的效果
代码解读
结尾 需要这些资料,可以 点击这里 领取

