这是我参与2022首次更文挑战的第41天,活动详情查看:2022首次更文挑战
React 函数组件 - 性能优化篇
前言
前段时间,在使用 React做H5项目的时候,使用Class组件
和 Hooks组件
开发项目的过程中,发现函数组件的props
不管有没有发生变化,只要父组件状态更改了,那么所有的子组件都会被更新,我们一起来看看吧
函数组件特性
- React中的hooks组件本质就是函数组件, 函数组件是由函数实现的,公式:F(Props) => UI;即
数据映射视图
- 对于函数组件来说,每次状态更新,都会导致组件更新
- 每次组件更新,组件内部是事件处理程序都会重新执行,子组件接受了新的props也会进行更新,这样就会产生一定的性能问题
举个栗子
import { useState } from "react";
import ReactDom from "react-dom";
function SubChildren() {
console.log("子组件更新了");
return <div>子组件</div>;
}
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildren />
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
- 这是一个计数器
+1
的栗子 - 页面刚加载的时候,子组件会被执行,没有问题
- 当我们执行
+1
操作的时候,SubChildren
组件依旧会被更新 - 此时的
SubChildren
组件不依赖外部的props
,当父组件执行+1
操作的时候,我们不希望子组件重新执行
React.memo
介绍
-
React.memo 本质是一个高阶组件。
-
如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
-
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
-
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现
代码改造
import { memo, useState } from "react";
import ReactDom from "react-dom";
type SubProps = {
name: string;
};
function SubChildren({ name }: SubProps) {
console.log("子组件更新了");
return <div>子组件 {name}</div>;
}
const SubChildrenMemo = memo(SubChildren);
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildrenMemo name="张三" />
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
- 这个时候子组件只有在第一次加载的时候会被调用
当React.memo
遇到了回调函数
import { memo, useState } from "react";
import ReactDom from "react-dom";
type SubProps = {
name: string;
onClick: () => void;
};
function SubChildren({ name, onClick }: SubProps) {
console.log("子组件更新了");
return <div onClick={onClick}>子组件 {name}</div>;
}
const SubChildrenMemo = memo(SubChildren);
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
const handleClickSub = () => {
console.log("子组件点击事件");
};
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildrenMemo name="张三" onClick={handleClickSub} />
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
- 当子组件添加了回调函数的时候,每次父组件执行
+1
操作的时候,子组件依旧会被更新 - 这个时候心里一万头那啥跑过
- 仔细阅读文档后发现
React.memo 默认情况下其只会对复杂对象做浅层对比
React.memo
+ UseCallback
解决事件回调问题
- 介绍
-
写法
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
-
返回一个 memoized 回调函数。
-
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
-
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
-
- 写法
import { memo, useCallback, useState } from "react"; import ReactDom from "react-dom"; type SubProps = { name: string; onClick: () => void; }; function SubChildren({ name, onClick }: SubProps) { console.log("子组件更新了"); return <div onClick={onClick}>子组件 {name}</div>; } const SubChildrenMemo = memo(SubChildren); function App() { console.log("App父组件"); const [count, setCount] = useState(0); const handleClickSub = useCallback(() => { console.log("子组件点击事件"); }, []); return ( <div> <p>{count}</p> <button onClick={() => setCount(count + 1)}>Click me</button> <SubChildrenMemo name="张三" onClick={handleClickSub} /> </div> ); } ReactDom.render(<App />, document.getElementById("root"));
说明
-
UseCallBack
接收俩个参数,第一个是事件处理函数,第二个是依赖项,没有依赖项就传入[]
,UseCallBack
的第二个参数和useEffect
的第二个参数具有相同的作用
如果在子组件的回调函数中处理父组件的属性,那么只需要添加对应的依赖项即可
- 举个栗子
const handleClickSub = useCallback(() => { console.log("子组件点击事件", count); }, [count]);
这样,父组件执行+1
操作的时候,子组件也就会自动更新了
- 流程是这样的
- 父组件执行
+1
操作,整个父组件会重新执行 UseCallBack
监听到count
发生了变化,会重新创建一个函数,传递给子组件- 子组件接受父组件传递的
props
,传入的函数引用地址和上一次函数的引用地址不一致,那么就会重新执行整个子组件 - 子组件执行流程:更新组件的事件处理程序 => 生成虚拟DOM树 => 对比前后俩次树结构变化 => 有变化则重新渲染页面;否则不做处理
你以为这就完事了么,不不不,别忘了,js的引用类型数据除了函数还有数组和对象,请看下边的栗子
import { memo, useCallback, useState } from "react";
import ReactDom from "react-dom";
type SubProps = {
name: string;
onClick: () => void;
user: {
name: string;
age: number;
};
};
function SubChildren({ name, onClick }: SubProps) {
console.log("子组件更新了");
return <div onClick={onClick}>子组件 {name}</div>;
}
const SubChildrenMemo = memo(SubChildren);
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
const handleClickSub = useCallback(() => {
console.log("子组件点击事件");
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildrenMemo
name="张三"
user={{ name: "李四", age: 18 }}
onClick={handleClickSub}
/>
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
说明
- 父组件传递了一个对象给子组件
- 由于父组件执行
+1
操作的时候, 父组件更新,都会创建一个新的对象 - 这个时候,子组件也会重新执行更新操作
React.memo
+ UseMemo
解决传递对象问题
说明
-
写法
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
返回一个 memoized 值。
-
把“创建”函数和依赖项数组作为参数传入
useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。 -
记住,传入
useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于useEffect
的适用范畴,而不是useMemo。
-
如果没有提供依赖项数组,
useMemo
在每次渲染时都会计算新的值。 -
你可以把
useMemo
作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有useMemo
的情况下也可以执行的代码 —— 之后再在你的代码中添加useMemo
,以达到优化性能的目的。 -
UseMemo 可以记住你想记住的任一类型数据
import { memo, useCallback, useMemo, useState } from "react";
import ReactDom from "react-dom";
type SubProps = {
name: string;
onClick: () => void;
user: {
name: string;
age: number;
};
};
function SubChildren({ name, onClick }: SubProps) {
console.log("子组件更新了");
return <div onClick={onClick}>子组件 {name}</div>;
}
const SubChildrenMemo = memo(SubChildren);
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
const _user = { name: "李四", age: 18 };
const user = useMemo(() => _user, []);
const handleClickSub = useCallback(() => {
console.log("子组件点击事件");
}, []);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildrenMemo name="张三" user={user} onClick={handleClickSub} />
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
说明
- 上面的代码中,我们把
user
对象信息提升到了顶部,方便使用 - 把
UseMemo
的返回结果传递给了子组件 - 这样父组件状态改变时,
useMemo
所依赖的的值没发生改变,当前函数就不会创建一个新的对象返回,子组件也就不会更新 useMemo
的第二个参数使用方式和useCallback
的第二个参数一样,都是依赖项,当依赖项发生改变,那么UseMemo
这个函数会重新执行,会返回一个新的引用类型的数据,由于子组件接受的props前后俩次值不一致,那么子组件就会被更新
useCallback
和 UseMemo
的区别
useCallback
第一个参数传入的是一个函数,无返回值UseMemo
第一个参数也是一个函数,但这个函数一定要有一个返回值- 因为
UseMemo
有返回值,那么首次执行父组件的时候,当前钩子也会被执行,useCallback
钩子在父组件首次加载的时候是不会被执行的
React.memo第二个参数
说明
React.memo第二个参数
是一个函数,函数中包含俩个参数,分别是上一次的props,和当前父组件传递下来的propsReact.memo第二个参数
必须有一个布尔
类型的返回值,如果返回true
那么组件不更新,返回false
组件更新,这和shouldComponentUpdate
的返回值恰恰相反- 本人不建议使用
React.memo
的第二个参数- 有现成的
hooks
足够我们应对绝大部分情况了,为啥还要手动实现一遍呢
- 有现成的
后记
- 上边我们说
UseMemo
可以记住任意类型 - 那把
UseCallBack
改成UseMemo
, 发现功能依旧ok
import { memo, useCallback, useMemo, useState } from "react";
import ReactDom from "react-dom";
type SubProps = {
name: string;
onClick: () => void;
user: {
name: string;
age: number;
};
};
function SubChildren({ name, onClick }: SubProps) {
console.log("子组件更新了");
return <div onClick={onClick}>子组件 {name}</div>;
}
const SubChildrenMemo = memo(SubChildren);
function App() {
console.log("App父组件");
const [count, setCount] = useState(0);
const _user = { name: "李四", age: 18 };
const user = useMemo(() => _user, []);
const handleClickSub = useMemo(
() => () => {
console.log("子组件点击事件");
},
[]
);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
<SubChildrenMemo name="张三" user={user} onClick={handleClickSub} />
</div>
);
}
ReactDom.render(<App />, document.getElementById("root"));
写在最后
- 希望大家收获满满