🔥 这些避免React组件重复渲染的手段你都知道吗?
使用React已经三年了,在这三年里面也沉积了不少关于React代码优化的最佳实践,今天先写一部分出来和大家分享分享。后续看文章是否受欢迎再觉得是否分享后面的。
这篇文章的每一个最佳实践我都会提供两个例子,一好一坏作为对比,和提供.gif
图片预览。
本片文章主要对以下这三种情况进行优化:
- 父组件更新导致子组件渲染
- Props的错误写法导致组件渲染
- Context的更新导致组件渲染
看完文章如果你觉得对你有了帮助,请帮忙点个赞,你的点赞是我创作的最大动力。评论点赞可以获得源码!!
父组件更新导致子组件渲染
Class 示例
❎ 错误示例预览
❎ 错误示例
import React, { Component } from "react";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { count } = this.state;
return (
<div className="parent">
<h5>错误示例</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
<Son />
</div>
);
}
}
class Son extends Component {
constructor(props) {
super(props);
}
render() {
console.log("子组件重新渲染了!!");
return <div className="son">子组件</div>;
}
}
export { Parent, Son };
在本示例中, 父组件中的state 发生变化导致了子组件的重新渲染,这样写的代码是一个很正常的写法,但是认真说起来,还是会造成性能上的浪费,毕竟子组件重新渲染了!接下来我们看看怎样解决这个问题!
说明: 本示例并不是说要杜绝写这样的代码,其实优化也是要看场景的!!
✅ 正确示例 1
import React, { Component, PureComponent } from "react";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { count } = this.state;
return (
<div className="parent">
<h5>正确示例1</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
<Son />
</div>
);
}
}
class Son extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log("子组件重新渲染了!!");
return <div className="son">子组件</div>;
}
}
export default Parent;
在这个例子中我们主要是借用了 PureComponent 继承这个类,React会自动帮我们执行 shouldComponentUpdate 对 Props 进行浅比较优化更新。
说明: 其实认真的讲,在React中组件会被 React.createElement(Son) 执行,所得到的组件的Props引用每次都是新的,因此会引发重新渲染!
✅ 正确示例 2
import React, { Component } from "react";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { count } = this.state;
const { children } = this.props;
return (
<div className="parent">
<h5>正确示例2</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
{children}
</div>
);
}
}
export default Parent;
<Parent>
<Son />
</Parent>
在本示例的优化中,我们将有状态组件和无状态组件进行分离,使用 children 将无状态组件传入。这样会避免无意义的重复渲染! 那为什么这样写会避免重新渲染呢? 因为直接在状态组件中使用children直接渲染子组件可以避免在状态组件中React使用React.createElement(Son) 渲染子组件!!这样也可以做到优化!
✅ 正确示例 3
import React, { Component, memo } from "react";
import { Son } from "./Bad";
const MemoSon = memo(() => <Son></Son>);
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { count } = this.state;
return (
<div className="parent">
<h5>正确示例3</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
<MemoSon />
</div>
);
}
}
export default Parent;
在本示例中其实实现优化的思想和示例1 所提到的思想类似, 我们借用了memo 函数,这个函数其实是为Function组件 准备的优化手段。我们在这里也是厚着脸皮强行使用一下!!,避免重新渲染的思想其实也是对比Props的引用。决定是否渲染!!
✅ 正确示例 4
import React, { Component, useState, Fragment } from "react";
import { Son } from "./Bad";
const ClickCount = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<Fragment>
<div>
<h5>正确示例4</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
</div>
</Fragment>
);
};
class Parent extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="parent">
<ClickCount />
<Son />
</div>
);
}
}
export default Parent;
本示例中,我们的优化手段主要是将状态组件提出去成一个组件,这样状态的改变就和子组件分离开了。也可以避免子组件的重新渲染!!
说明: 这个优化手段认真讲还是用到挺少的,看情况使用吧!!
Hooks 示例
错误示例预览
❎ 错误示例
import { useState } from "react";
const Son = () => {
console.log("子组件重新渲染了!!");
return <div className="son">子组件</div>;
};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>错误示例</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
<Son />
</div>
);
};
export { Son, Parent };
对于Hooks来说上面的写法也是非常正常的写法,但是与Class组件相比,Function组件 的特性是每一次的组件重新渲染,都会重新执行一次函数。而对于Class组件来说,只会执行一遍 new Class ,其实仔细想想还是挺可怕的。对函数组价来说,每次的执行都意味着新的上下文,新的变量,新的作用域。因此我们要更加注重函数组件的性能优化。
✅ 正确示例 1
import { useState } from "react";
const Parent = ({ children }) => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>正确示例1</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
{children}
</div>
);
};
export default Parent;
<Parent>
<Son />
</Parent
在本示例中,我们使用了children 直接渲染子组件,原理其实在上面Class组件示例中已经讲过了,这样的优化也能避免无意义的渲染。
说明: 认真的讲,结合函数组件的特性这个优化手段其实是治标不治本的!
✅ 正确示例 2
import { useState, useMemo } from "react";
import { Son } from "./Bad";
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>正确示例2</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
{useMemo(
() => (
<Son />
),
[]
)}
</div>
);
};
export default Parent;
在本示例中我们使用了useMemo 这个优化Hook,我们将 Son 组件进行缓存,只有当依赖改变,我们再去重新执行函数完成重新渲染,其他时机保证memoized相同,这样有助于避免在每次渲染时都进行高开销的计算。也避免了 每次在子组件中 都要重新声明变量,函数,作用域等。
说明:我觉得这个优化手段绝对称得上神来之笔,因为 useMemo 保存了组件的引用,没有重新执行函数组件,因此避免了组件内的变量,函数声明,和作用域的声明。从而优化了性能。 Nice!!
✅ 正确示例 3
import { useState, memo } from "react";
import { Son } from "./Bad";
const SonMemo = memo(Son);
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>正确示例3</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
<SonMemo />
</div>
);
};
export default Parent;
本示例中我们运用了memo这个api, 主要是对比props引用是否改变,从而避免子组件的重新渲染!
Props的错误写法导致组件渲染
Class 示例
❎ 错误示例预览
❎ 错误示例
import React, { Component, PureComponent } from "react";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { count } = this.state;
return (
<div className="parent">
<h5>错误示例</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
<Son componentDetails={{ name: "子组件" }} anyMethod={() => {}} />
</div>
);
}
}
class Son extends PureComponent {
constructor(props) {
super(props);
}
render() {
const { componentDetails, anyMethod } = this.props;
console.log("Son -> render -> anyMethod", anyMethod);
console.log("Son -> render -> componentDetails", componentDetails);
return <div className="son">{componentDetails?.name}</div>;
}
}
export { Parent, Son };
这个示例中Props的传递直接是错误的写法,为什么呢?因为组件的渲染主要是通过监听Props和State的变化进行渲染的,那在这个示例传的props每次都是一个新的对象,因为引用的不同,每次的父组件的渲染都会导致子组件的渲染。 因此这种写法造成的重新渲染实数不该!!
那我们应该怎么写呢?
✅ 正确示例 1
import React, { Component, PureComponent } from "react";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
componentDetails: { name: "子组件" },
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
anyMethod = () => {};
render() {
const { count, componentDetails } = this.state;
return (
<div className="parent">
<h5>正确示例 1</h5>
<p>父组件Count--{count}</p>
<button onClick={this.handleClick}>增加</button>
<Son componentDetails={componentDetails} anyMethod={this.anyMethod} />
</div>
);
}
}
class Son extends PureComponent {
constructor(props) {
super(props);
}
render() {
const { componentDetails, anyMethod } = this.props;
console.log("Son -> render -> anyMethod", anyMethod);
console.log("Son -> render -> componentDetails", componentDetails);
return <div className="son">{componentDetails?.name}</div>;
}
}
export default Parent;
本示例中,我们主要的正确写法就是直接将变量传递给子组件,因为变量的引用是相同的,因此经过PureComponent的检查,引用没有改变,从而阻止了子组件的渲染!!
说明: 严格来说,这个错误的示例是写法的问题,导致子组件的重新渲染,所以谈不上优化,因此我们要禁止像错误示例那样书写代码!
Hooks 示例
❎ 错误示例预览
❎ 错误示例
import { useState, useEffect } from "react";
const Son = ({ componentDetails, anyMethod }) => {
useEffect(() => {
console.log("Son -> componentDetails", componentDetails);
}, [componentDetails]);
useEffect(() => {
console.log("Son -> anyMethod", anyMethod);
}, [anyMethod]);
return <div className="son">{componentDetails.name}</div>;
};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>错误示例</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
<Son componentDetails={{ name: "子组件" }} anyMethod={() => {}} />
</div>
);
};
export { Son, Parent };
在这个错误示例当中,还是传递props的写法问题!!接下来看看如何改正!
✅ 正确示例 1
import { useState, useEffect } from "react";
const Son = ({ componentDetails, anyMethod }) => {
useEffect(() => {
console.log("Son -> componentDetails", componentDetails);
}, [componentDetails]);
useEffect(() => {
console.log("Son -> anyMethod", anyMethod);
}, [anyMethod]);
return <div className="son">{componentDetails.name}</div>;
};
// 这种写法 针对于 不变的值 可以这样传递
const componentDetails = { name: "子组件" };
const anyMethod = () => {};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>正确示例1</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
<Son componentDetails={componentDetails} anyMethod={anyMethod} />
</div>
);
};
export default Parent;
在这个示例中,我们只是将不变的值 提到组件之外 以保证引用的唯一,不会因为组件的更新而改变。但是这种写法有一定的局限性。就是只适合于不变的值。但也有效的避免了组件的重复渲染。
✅ 正确示例 2
import { useState, useEffect, useMemo, useCallback } from "react";
const Son = ({ componentDetails, anyMethod }) => {
useEffect(() => {
console.log("Son -> componentDetails", componentDetails);
}, [componentDetails]);
useEffect(() => {
console.log("Son -> anyMethod", anyMethod);
}, [anyMethod]);
return <div className="son">{componentDetails.name}</div>;
};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
const anyMethod = useCallback(() => {}, []);
const [componentDetails] = useMemo(() => {
const componentDetails = { name: "子组件" };
return [componentDetails];
}, []);
return (
<div className="parent">
<h5>正确示例2</h5>
<p>父组件Count--{count}</p>
<button onClick={handleClick}>增加</button>
<Son componentDetails={componentDetails} anyMethod={anyMethod} />
</div>
);
};
export default Parent;
在这个示例中,使用了 useCallback 于 useMemo 这个两个优化Hook,主要就是根据依赖是否改变来确定是否要更新值的变化,以保证值的引用不变。这中写法适合于大部分的写法,但是也不能过度使用。要不然代码会很混乱。
Context的更新导致组件渲染
Class 示例
❎ 错误示例预览
❎ 错误示例
import React, { Component, createContext } from "react";
const contextValue = createContext(undefined);
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
handleIncrement:this.handleIncrement
};
}
handleIncrement = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
return (
<contextValue.Provider
value={this.state}
>
<div className="parent">
<h5>错误示例</h5>
<Son1 />
<contextValue.Consumer>
{(conProps) => <Son2 conProps={conProps} />}
</contextValue.Consumer>
</div>
</contextValue.Provider>
);
}
}
class Son1 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("子组件 1 重新渲染了!!");
return <div className="son">子组件 1</div>;
}
}
class Son2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("子组件 2 重新渲染了!!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>子组件 2--{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
}
}
export { Parent };
在这个例子当中,仔细体会,当点击子组件2中的按钮时,改变的是父组件中的state 所以造成的问题就是 父组件的渲染导致子组件也渲染了。那应该怎样避免子组件的重复渲染呢?
✅ 正确示例 1
import React, { Component, createContext } from "react";
const contextValue = createContext(undefined);
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
handleIncrement:this.handleIncrement
};
}
handleIncrement = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
const { children } = this.props;
return (
<contextValue.Provider
value={this.state}
>
<div className="parent">
<h5>正确示例1</h5>
{children}
<contextValue.Consumer>
{(conProps) => <Son2 conProps={conProps} />}
</contextValue.Consumer>
</div>
</contextValue.Provider>
);
}
}
class Son1 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("子组件 1 重新渲染了!!");
return <div className="son">子组件 1</div>;
}
}
class Son2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("子组件 2 重新渲染了!!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>子组件 2--{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
}
}
export { Parent, Son1 };
<Parent>
<Son1 />
</Parent>
在这个示例中,我们还是借用了children 的机制,直接渲染,那么在父组件当中就没有 Ract.createElement(Son) 这个api的执行,因此就不会造成重复的渲染!
✅ 正确示例 2
import React, { Component, createContext, PureComponent } from "react";
const contextValue = createContext(undefined);
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
handleIncrement:this.handleIncrement
};
}
handleIncrement = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
render() {
return (
<contextValue.Provider
value={this.state}
>
<div className="parent">
<h5>正确示例2</h5>
<Son1 />
<contextValue.Consumer>
{(conProps) => <Son2 conProps={conProps} />}
</contextValue.Consumer>
</div>
</contextValue.Provider>
);
}
}
class Son1 extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log("子组件 1 重新渲染了!!");
return <div className="son">子组件 1</div>;
}
}
class Son2 extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log("子组件 2 重新渲染了!!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>子组件 2--{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
}
}
export default Parent;
在本示例中,主要是借用了 PureComponent 这个类帮我们自动执行优化,因此也是可以避免重复的渲染。
说明:这里你也可以强行使用一下React.memo。
Hooks 示例
❎ 错误示例预览
❎ 错误示例
import { createContext, useContext } from "react";
import { useCustomReducer } from "../useCustomizeContext";
const CustomizeContext = createContext(undefined);
const Son1 = () => {
console.log("子组件1 重新渲染了!!");
return <div className="son">子组件1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useContext(CustomizeContext);
console.log("子组件2 重新渲染了!!");
return (
<div className="son">
<p>子组件2-{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
};
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeContext.Provider value={value}>
<div className="parent">
<h5>错误示例</h5>
<Son2 />
<Son1 />
</div>
</CustomizeContext.Provider>
);
};
export { Son1, Parent, Son2 };
在本示例中 使用了createContext,useContext,useReducer 这几个api 实现了一个小型的Redux。再点击子组件2中的按钮,改变了count的值,进而导致 value 改变,因此父组件渲染,导致子组件也跟着渲染。
✅ 正确示例 1
import React from "react";
import {
CustomizeProvider,
useCustomizeContext,
useCustomReducer,
} from "../useCustomizeContext";
const Son1 = () => {
console.log("子组件1 重新渲染了!!");
return <div className="son">子组件1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("子组件2 重新渲染了!!");
return (
<div className="son">
<p>子组件2-{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
};
const Parent = ({ children }) => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>正确示例1</h5>
<Son2 />
{children}
</div>
</CustomizeProvider>
);
};
export { Son1 };
export default Parent;
<Parent>
<Son1 />
</Parent>
在本示例中我们依然是使用 children 来解决的重复渲染问题。这个方式还是很有效的!!
说明: 其实在项目中一定要使用合适的优化手段!
✅ 正确示例 2
import React, { memo } from "react";
import {
CustomizeProvider,
useCustomizeContext,
useCustomReducer,
} from "../useCustomizeContext";
const Son1 = () => {
console.log("子组件1 重新渲染了!!");
return <div className="son">子组件1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("子组件2 重新渲染了!!");
return (
<div className="son">
<p>子组件2-{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
};
// 使用 memo
const MemoSon1 = memo(Son1);
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>正确示例2</h5>
<Son2 />
<MemoSon1 />
</div>
</CustomizeProvider>
);
};
export default Parent;
在本示例中也使用了 memo 这个api, 还是一样的,对比props的引用是否改变,决定是否更新。
✅ 正确示例 3
import React, { useMemo } from "react";
import {
CustomizeProvider,
useCustomizeContext,
useCustomReducer,
} from "../useCustomizeContext";
const Son1 = () => {
console.log("子组件1 重新渲染了!!");
return <div className="son">子组件1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("子组件2 重新渲染了!!");
return (
<div className="son">
<p>子组件2-{count}</p>
<button onClick={handleIncrement}>增加</button>
</div>
);
};
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>正确示例3</h5>
<Son2 />
{useMemo(
() => (
<Son1 />
),
[]
)}
</div>
</CustomizeProvider>
);
};
export default Parent;
在这个示例中我们依然借助useMemo 这个优化Hook, 来进行对组件的优化。
🤙🤙🤙 总结
在本片文章中介绍了三种情况下的优化手段,主要就是使用了:
- 🤙useMemo
- 🤙memo
- 🤙children
- 🤙useCallback
- 🤙PureComponent
- 🤙提取状态组件
- 🤙提取不变的值
这些优化的手段可以在不同的情况使用,所以你如果在使用过程中,一定要结合代码的情况,使用合适的优化手段。
如果你还知道别的优化手段也可以在评论区进行留言哦!!!
👏👏👏 往期精彩
转载自:https://juejin.cn/post/7023172291622076447