🔥 🔥 🔥 这些避免 React 组件重复渲染的方法你知道吗?
使用 React 已经三年了,在这三年里面也沉淀了很多关于 React 代码优化的最佳实践,今天先写一部分出来分享给大家。看看文章热度怎么样,再看看后面的分享。
对于这篇文章中的每个最佳实践,我将提供两个示例,一个好的,一个坏的,以供比较,并提供图像预览.gif
。
本文主要针对这三种情况进行优化:
- 父组件更新导致子组件渲染
- Props 写法错误导致组件渲染
- 上下文更新导致组件渲染
看完文章如果觉得对您有帮助的话,请帮忙点个赞吧,您的点赞是我创作的最大动力。评论点赞即可获得源码!!!
父组件更新导致子组件渲染
类示例
❎ 错误示例预览
❎ 错误示例
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>Error Example</h5>
<p>Parent ComponentCount--{count}</p>
<button onClick={this.handleClick}>Add</button>
<Son />
</div>
);
}
}
class Son extends Component {
constructor(props) {
super(props);
}
render() {
console.log("Sub-component re-rendered!!!");
return <div className="son">Sub-components</div>;
}
}
export { Parent, Son };
在这个例子中,父组件的状态改变会导致子组件重新渲染,这是一种非常正常的代码编写方式,但说真的,这仍然会造成性能的浪费,毕竟子组件重新渲染了!接下来,我们看看如何解决这个问题!
注意:这个例子并不意味着不需要写这样的代码,其实优化也是依赖于场景的!
✅ 正确示例 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>Correct example 1</h5>
<p>Parent ComponentCount--{count}</p>
<button onClick={this.handleClick}>Add</button>
<Son />
</div>
);
}
}
class Son extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log("Sub-component re-rendered!!!");
return <div className="son">Sub-components</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>Correct example 2</h5>
<p>Parent Component Count--{count}</p>
<button onClick={this.handleClick}>Add</button>
{children}
</div>
);
}
}
export default Parent;
<Parent>
<Son />
</Parent>
在这个示例的优化中,我们分离了有状态组件和无状态组件,并使用children来传入无状态组件。这样可以避免无意义的重新渲染!那么,为什么这样写可以避免重新渲染呢?因为在有状态组件中直接使用
children可以避免在有状态组件中使用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>Correct example 3</h5>
<p>Parent Component Count--{count}</p>
<button onClick={this.handleClick}>Add</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>Correct example 4</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</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("Sub-component re-rendered!!!");
return <div className="son">Sub-components</div>;
};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>Error Example</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</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>Correct example 1</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</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>Correct example 2</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</button>
{useMemo(
() => (
<Son />
),
[]
)}
</div>
);
};
export default Parent;
在这个例子中,我们使用了优化钩子useMemo,缓存了子组件,只有当依赖项发生变化时,我们才会重新执行该函数来完成重新渲染,否则时间与memoized相同,这有助于避免每次渲染时进行高开销的计算。它还避免了每次都必须在子组件中重新声明变量、函数、作用域等。
注意:我认为这个优化非常出色,因为 useMemo 保存了组件引用,并且不会重新执行函数组件,从而避免了在组件内声明变量、函数和作用域。因此,性能得到了优化。太棒了!
✅ 正确示例 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>Correct example 3</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</button>
<SonMemo />
</div>
);
};
export default Parent;
在这个例子中我们使用了memo这个api ,主要是为了比较props引用是否发生了变化,从而避免子组件的重新渲染!
Props 写法错误导致组件渲染
类示例
❎ 错误示例预览
❎ 错误示例
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>Error Example</h5>
<p>Parent Component Count--{count}</p>
<button onClick={this.handleClick}>Add</button>
<Son componentDetails={{ name: "Sub-components" }} 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: "Sub-components" },
};
}
handleClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
};
anyMethod = () => {};
render() {
const { count, componentDetails } = this.state;
return (
<div className="parent">
<h5>Correct example 1</h5>
<p>Parent Component 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检查后,引用并没有发生改变,从而阻止了子组件的渲染!!!
注意:这个有bug的例子严格来说是一个书写问题,导致子组件重新渲染,根本谈不上优化,所以,我们还是禁止写类似有bug的例子那样的代码吧!
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>Error Example</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</button>
<Son componentDetails={{ name: "Sub-components" }} 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>;
};
// This is written for immutable values and can be passed like this
const componentDetails = { name: "Sub-components件" };
const anyMethod = () => {};
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount((old) => old + 1);
};
return (
<div className="parent">
<h5>Correct example 1</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</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: "Sub-components" };
return [componentDetails];
}, []);
return (
<div className="parent">
<h5>Correct example 2</h5>
<p>Parent Component Count--{count}</p>
<button onClick={handleClick}>Add</button>
<Son componentDetails={componentDetails} anyMethod={anyMethod} />
</div>
);
};
export default Parent;
本例中使用了useCallback和useMemo两个优化钩子,根据依赖项是否发生变化来判断是否更新值的变化,确保值的引用保持不变。这种方式适合大多数的写入,但不宜过度使用,否则代码会非常混乱。
上下文更新导致组件渲染
类示例
❎ 错误示例预览
❎ 错误示例
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>Error Example</h5>
<Son1 />
<contextValue.Consumer>
{(conProps) => <Son2 conProps={conProps} />}
</contextValue.Consumer>
</div>
</contextValue.Provider>
);
}
}
class Son1 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("Subcomponent 1 is re-rendered!");
return <div className="son">Subassembly 1</div>;
}
}
class Son2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("Subcomponent 2 is re-rendered!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>Subassembly 2--{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
}
}
export { Parent };
在这个例子中,如果你仔细观察,你会发现点击子组件 2 中的按钮时,其实是父组件的状态发生了变化,所以问题在于父组件的渲染会导致子组件也渲染。那么,如何避免子组件的重复渲染呢?
✅ 正确示例 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>Correct example 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("Subcomponent 1 is re-rendered!");
return <div className="son">Subassembly 1</div>;
}
}
class Son2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log("Subcomponent 2 is re-rendered!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>Subassembly 2--{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
}
}
export { Parent, Son1 };
<Parent>
<Son1 />
</Parent>
在这个例子中,我们仍然借用了children直接 render的机制,因此父组件中不存在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>Correct example 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("Subcomponent 1 is re-rendered!");
return <div className="son">Subcomponent 1</div>;
}
}
class Son2 extends PureComponent {
constructor(props) {
super(props);
}
render() {
console.log("Subcomponent 2 is re-rendered!");
const {
conProps: { count, handleIncrement },
} = this.props;
return (
<div className="son">
<p>Subcomponent 2--{count}</p>
<button onClick={handleIncrement}>Add</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("Subcomponent 1 re-rendered!!!");
return <div className="son">子组件1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useContext(CustomizeContext);
console.log("Subcomponent 2 re-rendered!!!");
return (
<div className="son">
<p>Subcomponent 2-{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
};
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeContext.Provider value={value}>
<div className="parent">
<h5>Error Example</h5>
<Son2 />
<Son1 />
</div>
</CustomizeContext.Provider>
);
};
export { Son1, Parent, Son2 };
本例中利用api的createContext,useContext,useReducer实现了一个小的Redux,在子组件2中点击按钮会改变count的值,进而导致值发生变化,于是父组件渲染,导致子组件也随之渲染。
✅ 正确示例 1
import React from "react";
import {
CustomizeProvider,
useCustomizeContext,
useCustomReducer,
} from "../useCustomizeContext";
const Son1 = () => {
console.log("Subcomponent 1 re-rendered!!!");
return <div className="son">Subcomponent 1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("Subcomponent 2 re-rendered!!!");
return (
<div className="son">
<p>Subcomponent 2-{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
};
const Parent = ({ children }) => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>Correct example 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("Subcomponent 1 re-rendered!!!");
return <div className="son">Subcomponent 1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("Subcomponent 2 re-rendered!!!");
return (
<div className="son">
<p>Subcomponent 2-{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
};
// use memo
const MemoSon1 = memo(Son1);
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>Correct example 2</h5>
<Son2 />
<MemoSon1 />
</div>
</CustomizeProvider>
);
};
export default Parent;
本例中也用到了api memo,还是一样的,比较props的引用是否发生了变化,决定是否更新。
✅ 正确示例 3
import React, { useMemo } from "react";
import {
CustomizeProvider,
useCustomizeContext,
useCustomReducer,
} from "../useCustomizeContext";
const Son1 = () => {
console.log("Subcomponent 1 re-rendered!!!");
return <div className="son">Subcomponent 1</div>;
};
const Son2 = () => {
const { count, handleIncrement } = useCustomizeContext();
console.log("Subcomponent 2 re-rendered!!!");
return (
<div className="son">
<p>Subcomponent 2-{count}</p>
<button onClick={handleIncrement}>Add</button>
</div>
);
};
const Parent = () => {
const value = useCustomReducer({ initValue: 1 });
return (
<CustomizeProvider value={value}>
<div className="parent">
<h5>Correct Example 3</h5>
<Son2 />
{useMemo(
() => (
<Son1 />
),
[]
)}
</div>
</CustomizeProvider>
);
};
export default Parent;
在这个例子中我们仍然使用useMemo优化钩子来优化组件。
🤙🤙🤙 摘要
本文主要针对三种情况下的优化手段进行阐述,主要使用。
- 🤙useMemo
- 🤙备忘录
- 🤙孩子们
- 🤙useCallback
- 🤙PureComponent
- 🤙提取状态组件
- 🤙提取常量值
这些优化可用于不同情况,因此如果您在将它们与代码结合使用时,必须使用适当的优化。
如果你还知道其他的优化手段也可以在评论区留下哦!
文章来源:https://dev.to/liujinyi/do-you-know-all-these-means-to-avoid-repeated-rendering-of-react-components-5mf