一旦父组件渲染,所有子组件都要跟着渲染,尽管这个子组件并没有任何改变。在这种情况下,这个子组件的渲染就变得多余。下面是几种处理方式:
shouldComponentUpdate
ShouldComponentUpdate生命周期函数内部通过props和state的浅比较来决定是否需要渲染,如果之前的prevProp和prevState跟当前的props,state
浅比较
相同的话,就会返回false,组件就不会进行渲染。
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
React.pureComponent
使用PureComponent会帮你内置一个ShouldComponentUpdate的生命周期, 例:
class Parent extends React.Component {
state = {
person: { name: '张三' },
name: 'hh',
};
componentDidMount() {
setTimeout(() => {
this.setState({ person: { name: '张三' }, name: 'hh' });
this.handleChange();
}, 3000);
}
handleChange = () => {};
render() {
return (
<Child
name={this.state.name} // 不会引起子组件改变
person={this.state.person} // 会引起改变
todo={this.handleChange} // 不会引起改变
/>
);
}
}
class Child extends React.PureComponent {
render() {
console.log('dfddddd');
return <div>{this.props.name}</div>;
}
}
export default Parent;
如果父组件是函数组件,可以使用Memo
配合useCallback
钩子函数,来避免重复渲染。
使用memo函数包裹子组件,而在使用函数的情况,需要考虑有没有函数传递给子组件使useCallback。
import Child from './child.tsx';
const App = () => {
const [state,setState] = useState({})
const handleTimerPickerChange = useCallback((hour: number, minute: number) => {
//复杂处理....
...
setState(s=>({a:a+1}))
}, []);
return (
...
<Child
onChange={handleTimerPickerChange}
/>
)
}
export default App
const Child:React.FC<> = () => {
return (
...
)
}
export default React.memo(Child)
避免使用内联对象
使用内联对象时,react会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于prop的浅层比较始终返回false,导致组件一直重新渲染。
可以利用ES6扩展运算符将传递的对象解构。这样组件接收到的便是基本类型的props,组件通过浅层比较发现接受的prop没有变化,则不会重新渲染。示例如下:
// Don't do this!
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />
}
// Do this instead
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: 'someValue' }
return <AnotherComponent style={styles} {...aProp} />
}
避免使用匿名函数
虽然匿名函数是传递函数的好方法(特别是需要用另一个prop作为参数调用的函数),但它们在每次渲染上都有不同的引用。
// 避免这样做
function Component(props) {
return <AnotherComponent onChange={() => props.callback(props.id)} />
}
// 优化方法一
function Component(props) {
const handleChange = useCallback(() => props.callback(props.id), [props.id]);
return <AnotherComponent onChange={handleChange} />
}
// 优化方法二
class Component extends React.Component {
handleChange = () => {
this.props.callback(this.props.id)
}
render() {
return <AnotherComponent onChange={this.handleChange} />
}
}
使用React.Fragment避免添加额外的DOM
有些情况下,我们需要在组件中返回多个元素,例如下面的元素,但是在react规定组件中必须有一个父元素。
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
如果这样做会创建额外的不必要的div。这会导致整个应用程序内创建许多无用的元素:
function Component() {
return (
<div>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</div>
)
}
实际上页面上的元素越多,加载所需的时间就越多。为了减少不必要的加载时间,我们可以使React.Fragment来避免创建不必要的元素。 Fragments简写形式<></>
function Component() {
return (
<React.Fragment>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</React.Fragment>
)
}
一些思考?
useState
该使用单个 state 变量还是多个 state 变量?
比如
//第一种
const [width, setWidth] = useState(100);
const [height, setHeight] = useState(100);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
//第二种:也可以这样把它们放在同一个obj里面,这样只需一个state
const [state, setState] = useState({
width: 100,
height: 100,
left: 0,
top: 0
});
不同:
1、如果使用单个 state 变量,每次更新 state 时需要合并之前的 state。因为 useState
返回的 setState
会替换原来的值。这一点和 Class 组件的 this.setState
不同。this.setState
会把更新的字段自动合并到 this.state
对象中。
2、使用多个 state 变量可以让 state 的粒度更细,更易于逻辑的拆分和组合
3、如果粒度过细,代码就会变得比较冗余。如果粒度过粗,代码的可复用性就会降低
总结:
- 将完全不相关的 state 拆分为多组 state。比如
size
和position
。 - 如果某些 state 是相互关联的,或者需要一起发生改变,就可以把它们合并为一组 state。比如
left
和top
,width
和height
。
const [position, setPosition] = useState({top: 0, left: 0});
const [size, setSize] = useState({width: 100, height: 100});
useCallBack&&useMemo
有时候我们会对创建函数开销进行评估。每次render 中创建函数可能会开销比较大,为了避免函数多次创建,使用了 useMemo
或者 useCallback
。但是对于现代浏览器来说,创建函数的成本微乎其微。因此,我们没有必要使用 useMemo
或者 useCallback
去节省这部分性能开销。当然,如果是为了保证每次 render 时回调的引用相等,你可以放心使用 useMemo
或者 useCallback
。
useMemo
useMemo
本身也有开销。useMemo
会「记住」一些值,同时在后续 render 时,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回「记住」的值。这个过程本身就会消耗一定的内存和计算资源。因此,过度使用 useMemo
可能会影响程序的性能。
要想合理使用 useMemo
,我们需要搞清楚 useMemo
适用的场景:
- 有些计算开销很大,我们就需要「记住」它的返回值,避免每次 render 都去重新计算。
- 由于值的引用发生变化,导致下游组件重新渲染,我们也需要「记住」这个值。
interface Props {
page: number;
type: string;
}
const App = ({page, type}: Props) => {
const result = useMemo(() => {
return getResult(page, type);
}, [page, type]);
return <child result={result}/>;
};
在上面的例子中,渲染 ExpensiveComponent
的开销很大。所以,当 resolvedValue
的引用发生变化时,作者不想重新渲染这个组件。因此,作者使用了 useMemo
,避免每次 render 重新计算 resolvedValue
,导致它的引用发生改变,从而使下游组件 re-render。
这个担忧是正确的,但是使用 useMemo
之前,我们应该先思考两个问题:
- 传递给
useMemo
的函数开销大不大?在上面的例子中,就是考虑getResolvedValue
函数的开销大不大。JS 中大多数方法都是优化过的,比如Array.map
、Array.forEach
等。如果你执行的操作开销不大,那么就不需要记住返回值。否则,使用useMemo
本身的开销就可能超过重新计算这个值的开销。因此,对于一些简单的 JS 运算来说,我们不需要使用useMemo
来「记住」它的返回值。 - 当输入相同时,「记忆」值的引用是否会发生改变?在上面的例子中,就是当
page
和type
相同时,resolvedValue
的引用是否会发生改变?这里我们就需要考虑resolvedValue
的类型了。如果resolvedValue
是一个对象,由于我们项目上使用「函数式编程」,每次函数调用都会产生一个新的引用。但是,如果resolvedValue
是一个原始值(string
,boolean
,null
,undefined
,number
,symbol
),也就不存在「引用」的概念了,每次计算出来的这个值一定是相等的。也就是说,ExpensiveComponent
组件不会被重新渲染。
因此,如果 getResolvedValue
的开销不大,并且 resolvedValue
返回一个字符串之类的原始值,那我们完全可以去掉 useMemo
,
因此,在使用 useMemo
之前,我们不妨先问自己几个问题:
- 要记住的函数开销很大吗?
- 返回的值是原始值吗?
- 记忆的值会被其他 Hook 或者子组件用到吗?
一、应该使用 useMemo
的场景
- 保持引用相等
- 对于组件内部用到的 object、array、函数等,如果用在了其他 Hook 的依赖数组中,或者作为 props 传递给了下游组件,应该使用
useMemo
。 - 自定义 Hook 中暴露出来的 object、array、函数等,都应该使用
useMemo
。以确保当值相同时,引用不发生变化。 - 使用
Context
时,如果Provider
的 value 中定义的值(第一层)发生了变化,即便用了 Pure Component 或者React.memo
,仍然会导致子组件 re-render。这种情况下,仍然建议使用useMemo
保持引用的一致性。
- 成本很高的计算
- 比如
cloneDeep
一个很大并且层级很深的数据
二、无需使用 useMemo 的场景
- 如果返回的值是原始值:
string
,boolean
,null
,undefined
,number
,symbol
(不包括动态声明的 Symbol),一般不需要使用useMemo
。 - 仅在组件内部用到的 object、array、函数等(没有作为 props 传递给子组件),且没有用到其他 Hook 的依赖数组中,一般不需要使用
useMemo
。
useMemo主要是用在子组件上而不是函数,函数用useCallback就可以了。如果子组件花销很大,useMemo可以避免由于父组件改变而导致子组件重新rerender。 useCallback 只是 useMemo 的一个语法糖,如果返回值是函数,都能用。当然,对于单个函数,用 useCallback 可能更方便。但是对于函数集合,用 useMemo 能更好地统一处理。
useMemo
和useCallback
不能盲目使用,因为他们都是基于闭包实现的,闭包会占用内存。- 当依赖项频繁改动时,要考虑
useMemo、useCallback
是否划算,因为useCallback
会频繁创建函数体。useMemo
会频繁创建回调。
自定义hooks
可以实现业务逻辑复用 例:多个组件需根据窗口大小来设置某个元素宽度
export default function useWidth() {
const [refWidth,setRefWidth]= useState(0)
useEffect(() => {
window.addEventListener('resize', handleResize); // 监听窗口大小改变
const clientW = document.documentElement.clientWidth;
handleResize(clientW)
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
handleResize = (clientW) => {
setRefWidth(clientW)
};
return refWidth
}