问答题676/1593说说你对 useMemo 的理解

难度:
2022-05-29 创建

参考答案:

Memo

在class的时代,我们一般是通过pureComponent来对数据进行一次浅比较,引入Hook特性后,我们可以使用Memo进行性能提升。

在此之前,我们来做一个实验

1import React, { useState } from "react"; 2import ReactDOM from "react-dom"; 3 4import "./styles.css"; 5 6function App() { 7 const [n, setN] = useState(0); 8 const [m, setM] = useState(10); 9 console.log("执行最外层盒子了"); 10 return ( 11 <> 12 <div> 13 最外层盒子 14 <Child1 value={n} /> 15 <Child2 value={m} /> 16 <button 17 onClick={() => { 18 setN(n + 1); 19 }} 20 > 21 n+1 22 </button> 23 <button 24 onClick={() => { 25 setM(m + 1); 26 }} 27 > 28 m+1 29 </button> 30 </div> 31 </> 32 ); 33} 34function Child1(props) { 35 console.log("执行子组件1了"); 36 return <div>子组件1上的n:{props.value}</div>; 37} 38function Child2(props) { 39 console.log("执行子组件2了"); 40 return <div>子组件2上的m:{props.value}</div>; 41} 42 43const rootElement = document.getElementById("root"); 44ReactDOM.render(<App />, rootElement);

上面的代码我设置了两个子组件,分别读取父组件上的n跟m,然后父组件上面设置两个点击按钮,当点击后分别让设置的n、m加1。以下是第一次渲染时log控制台的结果

执行最外层盒子了 
执行子组件1了 
执行子组件2了 

跟想象中一样,render时先进入App函数,执行,发现里面的两个child函数,执行,创建虚拟dom,创建实体dom,最后将画面渲染到页面上。

预览

使用Memo优化

当我点击n+1按钮时,此时state里面的n必然+1,也会重新引发render渲染,并把新的n更新到视图中。

预览
我们再看控制台

执行最外层盒子了 
执行子组件1了 
执行子组件2了 
+ 执行最外层盒子了 
+ 执行子组件1了 
+ 执行子组件2了 //为什么组件2也渲染了,里面的m没有变化 

你会发现子组件2也渲染了,显然react重新把所有的函数都执行了一遍,把未曾有n数据的子组件2也重新执行了。

如何优化?我们可以使用memo把子组件改成以下代码

1const Child1 = React.memo((props) => { 2 console.log("执行子组件1了"); 3 return <div>子组件1上的n:{props.value}</div>; 4}); 5 6const Child2 = React.memo((props) => { 7 console.log("执行子组件2了"); 8 return <div>子组件2上的m:{props.value}</div>; 9});

再重新点击试试?

执行最外层盒子了 
执行子组件1了 
执行子组件2了 
+ 执行最外层盒子了 
+ 执行子组件1了 

会发现没有执行子组件2了

这样的话react就会只执行对应state变化的组件,而没有变化的组件,则复用上一次的函数,也许memo也有memory的意思,代表记忆上一次的函数,不重新执行(我瞎猜的- -!!)

出现bug

上面的代码虽然已经优化好了性能,但是会有一个bug

上面的代码是由父组件控制<button>的,如果我把控制state的函数传递给子组件,会怎样呢?

1 <Child2 value={m} onClick={addM} /> //addM是修改M的函数

点击按钮让n+1

执行最外层盒子了 
执行子组件1了 
执行子组件2了 
+ 执行最外层盒子了 
+ 执行子组件1了 
+ 执行子组件2了 

又重新执行子组件2。

为什么会这样?因为App重新执行了,它会修改addM函数的地址(函数是复杂数据类型),而addM又作为props传递给子组件2,那么就会引发子组件2函数的重新执行。

useMemo

这时候就要用useMemo解决问题。

useMemo(()=>{},[])

useMemo接收两个参数,分别是函数和一个数组(实际上是依赖),函数里return 函数,数组内存放依赖。

1const addM = useMemo(() => { 2 return () => { 3 setM({ m: m.m + 1 }); 4 }; 5 }, [m]); //表示监控m变化

使用方式就跟useEffect似的。

useCallback

上面的代码很奇怪有没有

1useMemo(() => { 2 return () => { 3 setM({ m: m.m + 1 }); 4 }; 5 }, [m])

react就给我们准备了语法糖,useCallback。它是这样写的

1 const addM = useCallback(() => { 2 setM({ m: m.m + 1 }); 3 }, [m]);

是不是看上去正常多了?

最终代码

1import React, { useCallback, useMemo, useState } from "react"; 2import ReactDOM from "react-dom"; 3 4import "./styles.css"; 5 6function App() { 7 const [n, setN] = useState(0); 8 const [m, setM] = useState({ m: 1 }); 9 console.log("执行最外层盒子了"); 10 const addN = useMemo(() => { 11 return () => { 12 setN(n + 1); 13 }; 14 }, [n]); 15 const addM = useCallback(() => { 16 setM({ m: m.m + 1 }); 17 }, [m]); 18 return ( 19 <> 20 <div> 21 最外层盒子 22 <Child1 value={n} click={addN} /> 23 <Child2 value={m} click={addM} /> 24 <button onClick={addN}>n+1</button> 25 <button onClick={addM}>m+1</button> 26 </div> 27 </> 28 ); 29} 30const Child1 = React.memo((props) => { 31 console.log("执行子组件1了"); 32 return <div>子组件1上的n:{props.value}</div>; 33}); 34 35const Child2 = React.memo((props) => { 36 console.log("执行子组件2了"); 37 return <div>子组件2上的m:{props.value.m}</div>; 38}); 39 40const rootElement = document.getElementById("root"); 41ReactDOM.render(<App />, rootElement);

总结

  • 使用memo可以帮助我们优化性能,让react没必要执行不必要的函数
  • 由于复杂数据类型的地址可能发生改变,于是传递给子组件的props也会发生变化,这样还是会执行不必要的函数,所以就用到了useMemo这个api
  • useCallbackuseMemo的语法糖

最近更新时间:2024-08-10

赞赏支持

预览

题库维护不易,您的支持就是我们最大的动力!