反应的记忆化应该是最后的手段才是最好的解决方案
关于memoization的问题
当React中的父组件重新渲染时,子组件也会重新渲染。
举个例子,假设在父组件中每次点击“count up”按钮时,子组件ExpensiveComponent也会重新渲染。
当ExpensiveComponent包含了非常耗时的处理时,在每次按下计数按钮时都会触发该耗时的处理,导致性能下降。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponent/>
</div>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
为了防止这种行为,在此进行备忘录缓存。
通过对子组件进行记忆化的方式,即使父组件重新渲染,子组件也不会再次重新渲染。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
记忆化的问题
在将来的开发过程中使用memo化时,可能会出现实现上的小变化而导致无法按预期运行的担忧。
传递道具的模式
如果我们把style通过props传递给ExpensiveComponent ,会怎么样呢?
由於在傳遞給ExpensiveComponent時,一直是同一個物件,所以看起來符合預期的運作方式,但是由於父組件的重新渲染,子組件也會被重新渲染,導致無法實現memo化的效果。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo style={{"color":"red"}}/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({style}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p style={style}>expensive</p>
}
这是因为React的Props差异是通过Object.is进行比较的,如果是相同的则不会进行重新渲染,如果不相同则会被判断为有差异并进行重新渲染。(https://zh-hans.react.dev/reference/react/memo#minimizing-props-changes)
只有在两个值引用内存中的同一个对象时,Object.is才会返回true,因此在下面的情况下会返回false。
// falseになってしまう!!
Object.is({"color":"red"},{"color":"red"})
尽管对象的内容实际上并没有改变,但在React内部被判断为发生了变化,并触发了重新渲染。
为了解决这个问题,可以使用useMemo。
这样一来,memoStyle将会被视为同一个对象,即使使用Object.is进行比较,
子组件也不会再次重新渲染。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
const memoStyle = React.useMemo(()=>({"color":"red"}),[])
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo style={memoStyle}/>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({style}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p style={style}>expensive</p>
}
然而,当传递的props增加时,无法保证所有开发人员都能预料到这种行为,并正确地使用useMemo。有时候只有在检测到性能下降时才会意识到实现错误,这可能会导致陷入困境。
不注意这种陷阱可能会导致避免memo化的一个原因。
给予孩子们的场景。
我们也考虑将孩子交给他人的情况。
在这种情况下,虽然一开始看起来ExpensiveComponent的子元素是相同的,似乎会按照预期的方式工作,但是由于与父组件的渲染同步,子组件也会被渲染,导致代码无法进行memo化的功能。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<p>{count}</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponentMemo>
<p>children</p>
</ExpensiveComponentMemo>
</div>
);
}
const ExpensiveComponentMemo = React.memo(ExpensiveComponent);
function ExpensiveComponent({children}) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 1000;i++){
i= i+1;
}
return <div>expensive{children}</div>
}
在React中,JSX语法是React.createElement的语法糖,实际上会创建一个新的对象,因此会发生与之前相同的情况。
在这种情况下,也可以使用useMemo来防止重新渲染,但考虑到可读性和灵活性问题,这样的代码容易产生错误。
替代memo的选项
在我之前的描述中,我提到了记忆化可能存在意想不到的陷阱,特别是在多人合作开发和继承的项目中,很可能会不知不觉地引入错误并导致故障的可能性很大,因此建议尽量少使用。
但是,为了解决性能上的顾虑,我将介绍一种避免重新渲染的解决方案。
使用state分离组件
由于只有一部分使用了count的state,所以有一种方法是将这部分拆分到另一个组件中。
通过这个,受到 count 改变影响的组件会被限制在 CountComponent 上,所以 ExpensiveComponent 可以通过 count 的变化来避免重新渲染。
import React,{useState} from 'react';
export function App() {
return (
<div className='App'>
<h1>Hello React.</h1>
<CountComponent/>
<ExpensiveComponent/>
</div>
);
}
function CountComponent(){
[count,setCount]= useState(0);
return <p>{count}</p>
}
function ExpensiveComponent(props) {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return (
<>
<p>expensive</p>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
</>
)
}
使用children将组件分离
如果你想要保持这样的DOM结构,似乎很难将使用简单的state的组件分离开来。
import React,{useState} from 'react';
export function App() {
[count,setCount]= useState(0);
return (
<div className='App'>
<h1>Hello React.</h1>
<div className='count'>{count}</div>
<div>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
<ExpensiveComponent/>
</div>
</div>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
即使在这种情况下,通过将作为children的重型处理子组件传递给CountComponent并显示它,可以避免由于count的变化而导致ExpensiveComponent的重新渲染。
import React,{useState} from 'react';
export function App() {
return (
<div className='App'>
<h1>Hello React.</h1>
<CountComponent>
<ExpensiveComponent/>
</CountComponent>
</div>
);
}
dunction CountComponent({children}){
[count,setCount]= useState(0);
return (
<>
<div className='count'>{count}</div>
<div>
<button type="button" onClick={()=>setCount((count)=>count+1)}>count Up!!!</button>
{children}
</div>
</>
);
}
function ExpensiveComponent() {
console.log("rendering");
//なんらかの重い処理
for (let i = 0; i <= 100000;i++){
i= i+1;
}
return <p>expensive</p>
}
总结
在实施组件的记忆化时,虽然可以提高性能,但在后续的维护和运营阶段中很可能会无意中引入错误。
我认为首先要考虑是否可以在本次替代方案中实现而不进行记忆化的实现,这一点非常重要。
文献来源