目录React.memo示例介绍使用FAQReact.memo 二次优化小结useMemo示例介绍使用FAQ何时使用?示例示例小结扩展useCallbackReact.memo 这
这篇文章会详细介绍该何时、如何正确使用它,并且搭配 React.memo
来对我们的项目进行一个性能优化。
我们先从一个简单的示例入手
以下是一个常规的父子组件关系,打开浏览器控制台并观察,每次点击父组件中的 +
号按钮,都会导致子组件渲染。
const ReactNoMemoDemo = () => {
const [count, setCount] = React.useState(0);
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child name="Son" />
</div>
);
};
const Child = props => {
console.log('子组件渲染了');
return <p>Child Name: {props.name}</p>;
};
render(<ReactNoMemoDemo />);
子组件的 name
参数明明没有被修改,为什么还是重新渲染?
这就是 React
的渲染机制,组件内部的 state
或者 props
一旦发生修改,整个组件树都会被重新渲染一次,即时子组件的参数没有被修改,甚至无状态组件。
如何处理这个问题?接下里就要说到 React.memo
React.memo 是 React
官方提供的一个高阶组件,用于缓存我们的需要优化的组件
如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
让我们来改进一下上述的代码,只需要使用 React.memo 组件包裹起来即可,其他用法不变
function ReactMemoDemo() {
const [count, setCount] = React.useState(0);
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child name="Son" />
</div>
);
}
const Child = React.memo(props => {
console.log('子组件渲染了');
return <p>Child Name: {props.name}</p>;
});
render(<ReactMemoDemo />);
再次观察控制台,应该会发现再点击父组件的按钮,子组件已经不会重新渲染了。
这就是 React.memo
为我们做的缓存优化,渲染 Child
组件之前,对比 props
,发现 name
没有发生改变,因此返回了组件上一次的渲染的结果。
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
当然,如果我们子组件有内部状态并且发生了修改,依然会重新渲染(正常行为)。
看到这里,不禁会产生疑问,既然如此,那我直接为每个组件都添加 React.memo
来进行缓存就好了,再深究一下,为什么 React
不直接默认为每个组件缓存呢?那这样既节省了开发者的代码,又为项目带来了许多性能的优化,这样不好吗?
使用太多的缓存,反而容易带来 负提升。
前面有说到,组件使用缓存策略后,在被更新之前,会比较最新的 props
和上一次的 props
是否发生值修改,既然有比较,那就有计算,如果子组件的参数特别多且复杂繁重,那么这个比较的过程也会十分的消耗性能,甚至高于 虚拟 DOM
的生成,这时的缓存优化,反而产生的负面影响,这个就是关键问题。
当然,这种情况很少,大部分情况还是 组件树的 虚拟 DOM
计算比缓存计算更消耗性能。但是,既然有这种极端问题发生,就应该把选择权交给开发者,让我们自行决定是否需要对该组件进行渲染,这也是 React
不默认为组件设置缓存的原因。
也因此,在 React 社区中,开发者们也一致的认为,不必要的情况下,不需要使用 React.memo
。
什么时候该用? 组件渲染过程特别消耗性能,以至于能感觉到到,比如:长列表、图表等
什么时候不该用?组件参数结构十分庞大复杂,比如未知层级的对象,或者列表(城市,用户名)等
React.memo
默认每次会对复杂的对象做对比,如果你使用了 React.memo
缓存的组件参数十分复杂,且只有参数属性内的某些/某个字段会修改,或者根本不可能发生变化的情况下,你可以再粒度化的控制对比逻辑,通过 React.memo
第二个参数
function MyComponent(props) {
}
function shouldMemo(prevProps, nextProps) {
}
export default React.memo(MyComponent, shouldMemo);
如果对 class 组件有了解过的朋友应该知道,class 组件有一个生命周期叫做 shouldComponentUpdate()
,也是通过对比 props
来告诉组件是否需要更新,但是与这个逻辑刚好相反。
对于 React.memo
,无需刻意去使用它进行缓存组件,除非你能感觉到你需要。另外,不缓存的组件会多次的触发 render
,因此,如果你在组件内有打印信息,可能会被多次的触发,也不用去担心,即使强制被 rerender
,因为状态没有发生改变,因此每次 render
返回的值还是一样,所以也不会触发真实 dom
的更新,对页面实际没有任何影响。
同样,我们先看一个例子,calculatedCount
变量是一个假造的比较消耗性能的计算表达式,为了方便显示性能数据打印时间,我们使用了 IIFE
立即执行函数,每次计算 calculatedCount
都会输出它的计算消耗时间。
打开控制台,因为是 IIFE
,所以首次会直接打印出时间。然后,再点击 +
号,会发现再次打印出了计算耗时。这是因为 React
组件重渲染的时候,不仅是 jsx
,而且变量,函数这种也全部都会再次声明一次,因此导致了 calculatedCount
重新执行了初始化(计算),但是这个变量值并没有发生改变,如果每次渲染都要重新计算,那也是十分的消耗性能。
注意观察,在计算期间,页面会发生卡死,不能操作,这是 JS 引擎 的机制,在执行任务的时候,页面永远不会进行渲染,直到任务结束为止。这个过程对用户体验来说是致命的,虽然我们可以通过微任务去处理这个计算过程,从而避免页面的渲染阻塞,但是消耗性能这个问题仍然存在,我们需要通过其他方式去解决。
function UseMemoDemo() {
const [count, setCount] = React.useState(0);
const calculatedCount = (() => {
let res = 0;
const startTime = Date.now();
for (let i = 0; i <= 100000000; i++) {
res++;
}
console.log(`Calculated Count 计算耗时:${Date.now() - startTime} ms`);
return res;
})();
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<div>Calculated Count: {calculatedCount}</div>
</div>
);
}
const memoizedValue = useMemo(() => {
// 处理复杂计算,并 return 结果
}, []);
useMemo
返回一个缓存过的值,把 "创建" 函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized
值。这种优化有助于避免在每次渲染时都进行高开销的计算
第一个参数是函数,函数中需要返回计算值
第二个参数是依赖数组
这下就很好理解了,我们的 calculatedCount
没有任何外部依赖,因此只需要传递空数组作为第二个参数,开始改造
function UseMemoDemo() {
const [count, setCount] = React.useState(0);
const calculatedCount = useMemo(() => {
let res = 0;
const startTime = Date.now();
for (let i = 0; i <= 100000000; i++) {
res++;
}
console.log(`Memo Calculated Count 计算耗时:${Date.now() - startTime} ms`);
return res;
}, []);
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<div>Memorized Calculated Count: {calculatedCount}</div>
</div>
);
}
现在,"Memo Calculated Count 计算耗时"的输出信息永远只会打印一次,因为它被无限缓存了。
当你的表达式十分复杂需要经过大量计算的时候
下面示例中,我们使用状态提升,将子组件的 click
事件函数放在了父组件中,点击父组件的 +
号,发现子组件被重新渲染
const FunctionPropDemo = () => {
const [count, setCount] = React.useState(0);
const handleChildClick = () => {
//
};
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child onClick={handleChildClick} />
</div>
);
};
const Child = React.memo(props => {
console.log('子组件渲染了');
return (
<div>
<div>Child</div>
<button onClick={props.onClick}>Click Me</button>
</div>
);
});
render(<FunctionPropDemo />);
于是我们想到用 memo
函数包裹子组件,给缓存起来
const FunctionPropDemo = () => {
const [count, setCount] = React.useState(0);
const handleChildClick = () => {
//
};
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child onClick={handleChildClick} />
</div>
);
};
const Child = React.memo(props => {
console.log('子组件渲染了');
return (
<div>
<div>Child</div>
<button onClick={props.onClick}>Click Me</button>
</div>
);
});
render(<FunctionPropDemo />);
但是意外来了,即使被 memo
包裹的组件,还是被重新渲染了,为什么!
我们来逐一分析
+
号,count
发生变化,于是父组件开始重渲染useState
不会再初始化了, useEffect 钩子函数重新执行,虚拟 dom 更新Child
组件的时候,Child
准备更新,但是因为它是 memo
缓存组件,于是开始浅比较 props
参数,到这里为止一切正常Child
组件参数开始逐一比较变更,到了 onClick
函数,发现值为函数,提供的新值也为函数,但是因为刚刚在父组件内部重渲染时被重新初始化了(生成了新的地址),因为函数是引用类型值,导致引用地址发生改变!比较结果为不相等, React
仍会认为它已更改,因此重新发生了渲染。既然函数重新渲染会被重新初始化生成新的引用地址,因此我们应该避免它重新初始化。这个时候,useMemo
的第二个使用场景就来了
const FunctionPropDemo = () => {
const [count, setCount] = React.useState(0);
const handleChildClick = useMemo(() => {
return () => {
//
};
}, []);
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
<Child onClick={handleChildClick} />
</div>
);
};
const Child = React.memo(props => {
console.log('子组件渲染了');
return (
<div>
<div>Child</div>
<button onClick={props.onClick}>Click Me</button>
</div>
);
});
render(<FunctionPropDemo />);
这里我们将原本的 handleChildClick
函数通过 useMemo
包裹起来了,另外函数永远不会发生改变,因此传递第二参数为空数组,再次尝试点击 +
号,子组件不会被重新渲染了。
对于对象,数组,renderProps
(参数为 react
组件) 等参数,都可以使用 useMemo
进行缓存
既然 useMemo
可以缓存变量函数等,那组件其实也是一个函数,能不能被缓存呢?我们试一试
继续使用第一个案例,将 React.memo 移除,使用 useMemo
改造
const ReactNoMemoDemo = () => {
const [count, setCount] = React.useState(0);
const memorizedChild = useMemo(() => <Child name="Son" />, []);
return (
<div>
<div>Parent Count: {count}</div>
<button onClick={() => setCount(count => count + 1)}>+</button>
{memorizedChild}
</div>
);
};
const Child = props => {
console.log('子组件渲染了');
return <p>Child Name: {props.name}</p>;
};
render(<ReactNoMemoDemo />);
尝试点击 +
号,是的,Child
被 useMemo
缓存成功了!
同样的,不是必要的情况下,和 React.memo
一样,不需要特别的使用 useMemo
使用场景
useState
初始化,useState
不会二次执行,主要是函数参数)react
组件的缓存前面使用 useMemo 包裹了函数,会感觉代码结构非常的奇怪
const handleChildClick = useMemo(() => {
return () => {
//
};
}, []);
函数中又 return
了一个函数,其实还有另一个推荐的 api
, useCallback
来代替于对函数的缓存,两者功能是完全一样,只是使用方法的区别,useMemo
需要从第一个函数参数中 return
出要缓存的函数,useCallback
则直接将函数传入第一个参数即可
const handleChildClick = useCallback(() => {
//
}, []);
代码风格上简介明了了许多
看完这篇文章,相信你对 React.memo
和 React.useMemo
已经有了一定的了解,并且知道何时/如何使用它们了
以上就是React.memo React.useMemo对项目性能优化使用详解的详细内容,更多关于React.memo React.useMemo性能优化的资料请关注编程网其它相关文章!
--结束END--
本文标题: React.memo React.useMemo对项目性能优化使用详解
本文链接: https://lsjlt.com/news/177978.html(转载时请注明来源链接)
有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341
2024-01-12
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
2023-05-20
回答
回答
回答
回答
回答
回答
回答
回答
回答
回答
0