问题:添加一个新的元素,导致所有子组件重复渲染问题。
期望:子组件不要频繁的渲染,只渲染新增或发生变化数据的子组件。
解决方案:使用memo包囊子组件。
代码:
子组件Cat.js:
import React from 'react';
const Cat = ({name})=>{
console.log(`Cat init: ${name}`);
return <p>{name}</p>
}
export default Cat;
父组件CatList.js:
import React, { useState } from 'react';
import { Button } from 'antd';
import Cat from '../components/demo-cats/Cat'; // 没有使用memo
/**
* cat列表-父类
* @returns
*/
const CatList = () => {
console.log("cat 父类");
const [cats, setCats] = useState(["Cat 1","Cat 2","Cat 3"]);
// add
const handleAddCat = () => {
setCats([...cats, prompt("请输入cat名字")]);
}
return (
<div>
<h2>List of Cat</h2>
{
cats.map((name, index)=>(<Cat key={index} name={name}></Cat>))
}
<Button onClick={() => handleAddCat()}>Add Cat</Button>
</div>
);
};
export default CatList;
运行结果:
可以看到新增一个New Cat之后,cats数组长度发生了变化,导致父类重新计算state,所有的子组件重新渲染了一次。
解决思路:子组件用 React.memo() 包裹,如果组件在相同 props 的情况下渲染相同的结果,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
import React, { memo } from 'react';
import { Button } from 'antd';
const Cat = ({name, index, edit})=>{
console.log(`PureCat init: ${name}`);
return <p>{name} <Button onClick={() => edit(index)}>Edit</Button></p>
}
const PureCat = memo(Cat)
export default PureCat;
再次运行结果:
添加一个Memo Cat,只渲染了新增的子元素,不重复渲染之前的子元素。
再来看一个数组对象问题:
把上面cats改成数组对象,结合数组对象再来看一下数组的操作和调函数对子组件渲染的问题。
CatArray.js:
import React, { useState, useRef, useCallback } from 'react';
import { Button, Input } from 'antd';
// import Cat from '../components/demo-cats/Cat';
import Cat from '../components/demo-cats/PureCat';
// Cat数组对象 - demo
const CatArray = () => {
const [cats, setCats] = useState([{ name: "Cat 1" },{ name: "Cat 2" }, { name: "Cat 3" }]);
const refInput = useRef(null)
// add
const handleAddCat = () => {
var newCat = { name: refInput.current.input.value };
setCats([...cats, newCat]);
}
/**
* 属性发生变化,重新计算states
* @param {*} index
*/
const edit = (index) => {
const catsCopy = [...cats]; //浅拷贝一下
// setCats(catsCopy.map((item, key) => key === index ? { ...item, name: "Jony" } : item));
catsCopy.splice(index, 1, {...catsCopy[index], name: "Jony"});
setCats(catsCopy)
}
return (
<div>
<h2>Array Object of Cat</h2>
{
cats.map((item, index) => (<Cat key={index} edit={edit} index={index} name={item.name}></Cat>))
}
<Input ref={refInput} placeholder="Please input cat name"></Input>
<Button onClick={() => handleAddCat()}>Add Cat</Button>
</div>
);
};
备注:Cat是使用memo包囊的子组件。
新增一个名字为New Cat的Cat,添加到数组之后,即使memo包囊的子组件PureCat,子组件依然被重复渲染了。为什么?因为cats发生变化,state重新计算,导致父组件重新渲染,导致edit重新生成了新的方法,子组件props中edit发生了变化,导致子组件重新被渲染了。
React相关的知识:
渲染(Render)
Render基于虚拟 DOM 和高效 Diff 算法的完美配合,实现了对 DOM 最小粒度的更新。
react 处理 render 的基本思维模式是每次一有变动就会去重新渲染整个应用。会将 render 函数返回的虚拟 DOM 树与老的进行比较,从而确定 DOM 要不要更新、怎么更新。
何时触发渲染(Render)
组件挂载
setState() 方法被调用 ( 当 setState 传入 null 的时候,并不会触发 render )
使用React.useCallback解决父组件state发生变化,回调函数重新计算的问题。
/**
* useCallback依赖空数组,初始化时调用一次
*/
const editCallBack = useCallback((index) => {
const catsCopy = [...cats]; //浅拷贝一下
// setCats(catsCopy.map((item, key) => key === index ? { ...item, name: "Jony" } : item));
catsCopy.splice(index, 1, {...catsCopy[index], name: "Jony"});
setCats(catsCopy)
}, []);
使用useCallback把edit方法包起来,useCallback依赖空数组,初始化时生成子组件依赖的回调函数,回调函数仅在某个依赖项改变时才会更新。
总结
父组件中state(状态)改变,不受memo保护的子组件也会重新渲染
memo会检测props到改变来决定组件是否需要进行重新渲染,换言之就是,被memo函数包起来的组件只有本身的props被改变之后才会重新渲染
memo只能进行浅拷贝来校验决定是否触发重新渲染。所以改变数组(对象)的props时候记得返回一个全新的数组(对象)
memo不是项目中所有的组件都需要包一下。包的太多反而会起反效果,我们需要选择那些经常被重新渲染的组件有选择性的去缓存。
参考链接: