React下memo, useMemo, useCallback的使用区别解析

前言

欢迎来到知多少栏目,我是今天的主持人【赵花花5070】,今天邀请了三位神秘嘉宾,分别是memo(), useMemo(), useCallback()

在正式介绍三位嘉宾之前,我们先去看看他们平时的工作环境是怎么样的,请看示例。

环境

基础环境

  1. NodeJs

    v16.13.2

  2. React

    “react”: “^18.2.0”

    “react-dom”: “^18.2.0”

目录结构

>  ...
> src

不知道大家对这三位嘉宾有多少了解,但不论是了解还是不了解的,都请容许我在这里再次介绍一下。

memo()

memo 主要用来优化函数组件的重复渲染行为,针对的是一个组件

光说是没有用的,我们拿个示例来看看,到底有什么样的效果。

1. 基础使用

使用memo缓存组件

const MemoChild = memo((props) => {
  console.log('子组件被重新渲染了')
  return <div>子组件</div>
})

左侧用于触发事件的组件

const LeftPage = (props) => {
  const {fn} = props;
  const [count, setCount] = useState(0);

  return (
    <div>
      <div>left</div>
      <button onClick={() => {
        let num = count + 1;
        setCount(num)
        fn(num)
      }}>点击::</button>
    </div>
  )
}

父组件

const App = () => {
  	console.log("============重新渲染分隔符============")
	console.log("父组件==》》")
	const [count, setCount] = useState(0);
	const changeFn = (val) => setCount(val);
	return (
		<>
			<div>
				<div>count值变化::{count}</div>
			</div>
			<LeftPage fn={changeFn} />
			<MemoChild/>
		</>
	)
}

上述代码运行结果如下:

在这里插入图片描述

可以看到每一个组件都被渲染两次,这是因为React的严格模式导致的,react严格模式是为了在我们开发过程中帮助我们发现小bug,通过故意重复调生命周期函数让我们发现问题。

需要注意的一点是这仅适用于开发模式生产模式下生命周期不会被调用两次

如果不想要渲染两次就直接把src目录下入口文件index.js文件中的<React.StrictMode>标签给注释或者拿掉就行。

那么此处就不深究那么多(这不是本次笔记的重点),依然使用严格模式

此时通过另外一个组件去触发变更count值,观察一下使用memo包装的组件是否再次渲染

在这里插入图片描述
可以看到使用memo包装的组件没有再次渲染,这样子就起到了一个组件缓存的作用,但实际项目中,有的组件是想要缓存但是又有依赖属性,这又如何使用,又能否实现呢?

请接着往下看。

2. 避雷踩坑

  • 避雷点1 —> 传递固定不变number类型参数

    在上面1的代码基础上稍作修改,其他组件不变化,再观察一下运行结果有什么变化。

    memo包裹的组件打印输出父组件传递的属性a

    const MemoChild = memo((props) => {
      console.log('子组件被重新渲染了')
      return <div>子组件{props.a}</div>
    })
    

    App组件新增属性 a = {1}

    ...
    return (
    	<>
    		...
    		<MemoChild a={1}/>
    		...
    	</>
    )
    

    给memo包裹的组件新增属性a,那么此时再次触发变更count值事件,那么memo又有什么变化呢?

    在这里插入图片描述
    连续触发两次变更事件后通过截图可以看出,跟上面不传递参数的输出并没有什么区别,为什么呢?

    不着急,接着往下看,最后我们再来总结一下捋一捋。

  • 避雷点2 —> 传递一个布尔值

    App组件新增属性 a={true}

    ...
    return (
    	<>
    		...
    		<MemoChild a={true}/>
    		...
    	</>
    )
    

    在这里插入图片描述
    传递一个固定不变的布尔类型的数据也没有让memo包裹的组件再次渲染。

  • 避雷点3 —> 传递改变的参数(count)

    上面既然传递的是一个固定不变的值,那这次传递一个变化的值会怎么样呢?

    继续在上次的代码的基础上稍微改吧改吧。

    App组件新增属性 a = {count}

    ...
    return (
    	<>
    		...
    		<MemoChild a={count}/>
    		...
    	</>
    )
    

    在这里插入图片描述
    可以发现随着每次事件的触发,count值的更新,被memo包裹的组件也跟着再次渲染,这又是为什么呢?

  • 避雷点4 —> 传递一个函数

    App组件新增属性 a={() => {}}

    ...
    return (
    	<>
    		...
    		<MemoChild a={() => {}}/>
    		...
    	</>
    )
    

    在这里插入图片描述

  • 避雷点5 —> 传递一个数组

    App组件新增属性 a={[1,2,3,4,5]}

    ...
    return (
    	<>
    		...
    		<MemoChild a={[1,2,3,4,5]}/>
    		...
    	</>
    )
    

    在这里插入图片描述

  • 避雷点6 —> 传递一个对象

    App组件新增属性 a={{a:1, b: 2}}

    ...
    return (
    	<>
    		...
    		<MemoChild a={{a:1, b: 2}}/>
    		...
    	</>
    )
    

    在这里插入图片描述

3.memo总结

通过上述所有示例运行结果来看,这个就不难看出来是跟数据类型有关系的。

  1. 传递不变的基础类型的值,组价不会再次渲染
  2. 传递会变化的基础类型的值,组件会再次渲染
  3. 传递引用数据类型的值,组件会再次渲染

通过上面三条观察结果,可以得出,每次组件是否发生变化,是与组件接收的参数 props 里面的数据指向内存中映射地址是有关系的。如果这个地址发生了变化,那么组件会再次渲染,如果没有变化,则不渲染。

useMemo()

useMemo 返回的是一个

说明白了memo,接下来说一下useMemo,这个就不是对应的组件,请看示例。

1. 基础示例

const App = () => {
  	console.log("============重新渲染分隔符============")
	console.log("父组件==》》")
	const [count, setCount] = useState(0);
	const changeFn = (val) => setCount(val);
	
	const a = 1;	

	const result = useMemo(() => {
	   console.log('useMemo被执行了')
	   return a + 1;
	 }, [a])
	
	return (
		<>
			<div>
				<div>count值变化::{result}</div>
			</div>
			<LeftPage fn={changeFn} />
		</>
	)
}

在这里插入图片描述
可以看的出来,监听一个不变且函数体内进行计算的值也不变的值,除了初始化的时候渲染执行,其后的每次事件触发都没有再次执行,说明这是起到了一个缓存的目的。

这时候我突发奇想,如果我监听一个变化,但函数体内进行计算的值是变化
的,那输出又是怎么样的呢?

2. 监听不变参数

...

const result = useMemo(() => {
	console.log('useMemo被执行了')
	return count + 1;   // 函数体内进行计算的值count
}, [a])  // 监听的值a

return (
	<>
		...
		<div>
			<div>count值变化::{result}</div>
		</div>
		...
	</>
)

在这里插入图片描述
这个运行结果与上面的示例没有什么不同,那么接下来再看看下面这个示例。

3. 监听变化参数

  const result = useMemo(() => {
    console.log('useMemo被执行了')
    return a + 1;   // 函数体内进行计算的值a
  }, [count])  // 监听的值count

在这里插入图片描述

4. useMemo总结

通过上面三个示例,大家应该就能够完全明白这个的具体使用方法及注意事项,这里的函数体内是否执行的关键是看 useMemo(() => {}, [ params1,params2,… ]) 第二个参数里面的值是否发生变化

注意:即使jsx中没有使用useMemo返回的值,初始化及监听数据发生变化的时候还是会执行,不会起到缓存的目的。

useCallback()

useCallback 返回的是一个函数,如果需要使用则需要调用这个函数。

都说这个useCallback具有对函数体缓存的作用,如果再次触发就不会再创建这个函数,然而实时上不是的。真相往往会让人大吃一惊。

基础使用

	const changeFn = useCallback(() => {
		// 可以做一些事情
	},[params1,params2,...])

只有当params1 或者 params2中的任意一个依赖项发生变化,就会执行这个useCallback()函数。

是不是以为只要使用useCallback包裹的函数,就能够进行缓存,不再被创建新的函数体,继续执行上一次的函数,不论依赖项是否发生变化,useCallback里面的函数体逻辑都走缓存

不用多想,我就知道很多人都是这么想的,我以前在项目中也有这么使用过。

可是再一想,不对呀,如果都走缓存,为什么官方不直接使用缓存技术,还特意创建一个hooks api?

实时上有点想当然了,实际上的初始化的时候会在内存中申请一篇空间创建一个函数,这就跟创建普通函数一模一样,即使使用useCallback。

当数据发生变化时,上一次没有使用useCallback包裹的函数会被丢掉,重新创建内存空间进行存储。

而采用useCallback包裹的函数不但要执行useCallback额外的函数,还要在内存中创建一个新的空间用于函数存储。为了判断数据是否需要使用缓存,上次创建的不会被丢弃反而会存储起来,最后会对上次与本次创建的函数进行对比,如果依赖项没有发生变化则会直接返回上一次的计算结果,否则会重新计算。

需要注意的是,如果useCallback函数体中使用了组件中其他的依赖项,那么这个数据会一直存储在内存中不会被销毁。

如下示例:

const a = 1;

const fn = useCallback(() => {
	return a + 1;
}, [])

上面例子中声明的常量 a 本来是可以使用完就销毁掉的,但是因为 fn 函数里面使用到了这个 a 常量,那么即使在 fn 函数使用完成后 a 常量也不会销毁掉。

这么一看使用useCallback一点好处也没有,并且如何正确使用,怎么才算是正确使用?

其实这个hook主要是为了解决,父组件有更新,子组件没有依赖项,子组件不更新(不重新计算)的问题而设计的。

但真正使用起来的时候并没有我们所想想的那么好使,一般需要搭配 memo 一起使用,啥意思呢?

就是说当使用memo缓存的子组件,其参数props同样被缓存,如果这个props没有发生变化,则子组件不会重新渲染也就不会造成useCallback里面重复计算,这样就能够起到一个优化的目的,但是一个函数计算一般都是很快,主要问题一般都是出在渲染问题上。

通过一段代码来验证一下。

const MemoChild = memo((props) => {
  console.log('子组件被重新渲染了')
  return <CenterPage type={1} fn={props.fn}/>
})
const App = () => {
	console.log("============重新渲染分隔符============")
	console.log("父组件==》》")
	
	const handleClick = useCallback(() => {
		console.log('useCallback被执行了')
	}, [])

	return (
		<div className="app">
			{/* <LeftPage name={name} fn={changeNameFn} /> */}
			<div>
				<div>{name}</div>
				<button onClick={() => {
				changeNameFn(name+'_111')
				}}>点击::</button>
			</div>
			
			<CenterPage fn={handleClick}/>
			{/* <MemoChild fn={handleClick}/> */}
		</div>
	);
}

在这里插入图片描述
因为fn传递的是一个函数,所以每次父组件状态更新,那么子组件必然要再次渲染。

那么把上面的代码稍微修改一下呢?

...
const result = useMemo(() => {
	console.log('useMemo被执行了')
	return count + 1;   // 函数体内进行计算的值count
}, [a])  // 监听的值a

return (
	<>
		...
		{/* <CenterPage fn={handleClick}/> */}
		<MemoChild fn={handleClick}/>
	</>
)

在这里插入图片描述

从上述运行结果中不难看出子组件没有再次渲染,那么就不存在useCallback包裹的函数重新渲染,所以使用的还是上次缓存下来的函数。

但是如果useCallback依赖的参数发生了变化,那么也就起不到什么作用了,会再次渲染一次,函数也会再次进行计算丢掉上次缓存的。

可以在上次代码的基础上再修改修改,接着往下看。

const handleClick = useCallback(() => {
	console.log('useCallback被执行了')
}, [name])

在这里插入图片描述

最后为了始终保持函数缓存使用,可以考虑 ahooksuseMemoizedFn1 函数,从而达到持久化fn。

总结

useMemo()useCallback() 都接收两个参数,第一个参数为函数(fn),第二个参数为数组([]),数组中是变化依赖的参数2

memo() 则可以直接作用于组件

不建议大量使用memo, useMemo, useCallback 去做缓存,因为会增加大量无效工作导致重复渲染,特别是初次渲染增加极大负担,使代码变得难以维护不容易被理解。


  1. 持久化 function 的 Hook,理论上,可以使用 useMemoizedFn 完全代替 useCallback。 ↩︎

  2. 数组中的数据如果发生了变化则会执行 useMemo()useCallback() 中的第一个参数函数体↩︎

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值