前言
有很多同学已经在工作中用上了React Hook
进行项目开发,相信大家也体会到了Hook
的美妙之处,但是无论基于什么技术栈进行开发,性能方面总是大家关心的一个话题,本文将会介绍如何通过
- useCallback + memo 避免组件不必要重的重复渲染
- useMemo 避免组件在每次渲染时都进行高开销的计算
当然也会对
useCallback
,useMemo
的源码进行浅析。不足的地方希望大神能够指点一二~
优化前
定义一个子组件
const ChildComponent = () => {
return (
<Fragment>
{console.log("ChildComponent Render")} {/* {1} */}
<span>子组件</span>
</Fragment>
)
}
- 我们这个组价相当简单,它没有接受任何属性,而我们在
{1}
[注]{1}对应代码后面注释号,下面不再赘述 的地方打印了ChildComponent Render
我们希望这个组件只在挂载的时候执行render
函数,其他时候则无需执行,在工作中业务组件相对会复杂一些,不必要的render
也是会影响性能的。
定义一个父组件
import React, { Fragment, useState } from "react";
import { Button, Tag, Divider } from "antd";
const ParentComponent = () => {
const [count, setCount] = useState<number>(0); //{1}
return (
<Fragment>
<h5>hooks 性能优化篇</h5>
<Divider orientation="left">count</Divider>
<Tag color="magenta">{count}</Tag>
<Button type="primary" onClick={() => setCount((o: number) => o += 1)}>setCount</Button> {/* {2} */}
<Divider orientation="left">子组件↓</Divider>
<ChildComponent /> {/* {3} */}
</Fragment>
)
}
- 以上,我们在
ParentComponent
组件中定义了一个count
{1}
, 它是一个useState
, 在{2}
改变了count
的值使其+1 , 同时在{3}
使用了上面定义的ChildComponent
组件 - 我们知道
react
在setState
之后组件会重新render
,我们将在{2}
处setCount
并查看ChildComponent
组件的render
函数是否执行。 - 结果是显而易见的,随着
ParentComponent
组件setCount
,ChildComponent
的render
函数也会被执行,这很不合理。对于这种场景来说,我们只希望ChildComponent
的render
函数在挂载的时候执行。下面我们开始优化。
useCallback + memo 避免组件不必要重的重复渲染
首先我们需要介绍一下 memo
和 useCallback
的概念
memo
- 使用过
class
组件进行开发的同学知道PureComponent
,是的,memo
和PureComponent
做的事情几乎是一样的 React.memo
为高阶组件,的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果- 总结一下,通过
React.memo
包裹的组件props
相同(注意这里是浅比较哦)的情况下,会复用最近一次执行的结果,真棒,React.memo
帮我们缓存了组件
useCallback
当前还没到useCallback
的场景,我们先介绍功能
- 官方文档
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将非常有用。
- 总结一下,
useCallback
帮我们缓存了函数,在依赖项没有变化的时候返回缓存的函数指针,而props
涉及到复杂对象类型都是通过指针来传递的。
优化后
import React, { Fragment, useState, memo } from "react";
import { Button, Tag, Divider } from "antd";
// 父组件
const ParentComponent = () => {
const [count, setCount] = useState<number>(0); //{1}
return (
<Fragment>
<h5>hooks 性能优化篇</h5>
<Divider orientation="left">count</Divider>
<Tag color="magenta">{count}</Tag>
<Button type="primary" onClick={() => setCount((o: number) => o += 1)}>setCount</Button> {/* {2} */}
<Divider orientation="left">子组件↓</Divider>
<ChildComponent /> {/* {3} */}
</Fragment>
)
}
// 子组件
const ChildComponent = memo(() => { {/* + {2} */}
return (
<Fragment>
{console.log("ChildComponent Render")} {/* {1} */}
<span>子组件</span>
</Fragment>
)
})
- 以上,我们在
ChildComponent
组件,新增了{2}
,使用React.memo
对组件进行包裹,在ParentComponent
组件{2}
处setCount
之后查看ChildComponent render
函数是否执行,结果如下图,并不会重复执行
丰富一下业务场景
import React, { Fragment, useState, memo } from "react";
import { Button, Tag, Divider } from "antd";
const ParentComponent = () => {
const [count, setCount] = useState<number>(0); //{1}
const [random, setRandom] = useState<number>(0); // { + 4}
function childFn() { // {+6}
}
return (
<Fragment>
<h5>hooks 性能优化篇</h5>
<Divider orientation="left">count</Divider>
<Tag color="magenta">{count}</Tag>
<Button type="primary" onClick={() => setCount((o: number) => o += 1)}>setCount</Button> {/* {2} */}
<Divider orientation="left">random</Divider>
<Tag color="cyan">{random}</Tag>
<Button type="ghost" onClick={() => setRandom(Math.floor(Math.random() * 10 + 1))}>setRandom</Button> {/* + {5} */}
<Divider orientation="left">子组件↓</Divider> {/* {3} */}
<ChildComponent state={random} fn={childFn} /> {/* + {7} */}
</Fragment>
)
}
interface ChildProps {
state: number,
fn: Function
}
const ChildComponent = memo((props: ChildProps) => {
const { state } = props;
return (
<Fragment>
{console.log("ChildComponent Render")} {/* {1} */}
<span>子组件</span>
<Tag color="magenta">{state}</Tag>
</Fragment>
)
})
- 我们丰富一下业务场景,在
ParentComponent
新增了{4} {+6}
分别为一个随机数,一个函数作为ChildComponent
的props
,并且在{5}
修改了随机数的值,现在我们看下ChildComponent render
函数的执行情况 - 理想情况是只有在
random
变化的时候ChildComponent render
函数执行,而结果并不是这样,在我们修改count
的时候ChildComponent render
函数也会执行,结果如下图
丰富业务场景后优化
- 造成上面的问题是因为我们在
ChildComponent
的props
中增加了函数fn
,上面说过props
在 传递函数的时候是传递指针,而随着ParentComponent
中setCount
函数childFn
会被重新声明,指针也会相应更新,这时候大家应该想到useCallback
了 - 现在我们将
ParentComponent
中的childFn
用useCallback
进行缓存
import React, { Fragment, useState, memo, useCallback } from "react";
import { Button, Tag, Divider } from "antd";
const ParentComponent = () => {
const [count, setCount] = useState<number>(0); //{1}
const [random, setRandom] = useState<number>(0); // {4}
const memoizedFn = useCallback(childFn, []); // {+ 8}
function childFn() { // {6}
}
return (
<Fragment>
<h5>hooks 性能优化篇</h5>
<Divider orientation="left">count</Divider>
<Tag color="magenta">{count}</Tag>
<Button type="primary" onClick={() => setCount((o: number) => o += 1)}>setCount</Button> {/* {2} */}
<Divider orientation="left">random</Divider>
<Tag color="cyan">{random}</Tag>
<Button type="ghost" onClick={() => setRandom(Math.floor(Math.random() * 10 + 1))}>setRandom</Button> {/* {5} */}
<Divider orientation="left">子组件↓</Divider> {/* {3} */}
<ChildComponent state={random} fn={memoizedFn} /> {/* {+ 7} */}
</Fragment>
)
}
interface ChildProps {
state: number,
fn: Function
}
const ChildComponent = memo((props: ChildProps) => {
const { state } = props;
return (
<Fragment>
{console.log("ChildComponent Render")} {/* {1} */}
<span>子组件</span>
<Tag color="magenta">{state}</Tag>
</Fragment>
)
})
-
如上,我们在
ParentComponent
组件中新增了{8}
,将函数用useCallback
进行了缓存,并且更新了{7}
的用法 -
结果如下图,是我们期望的结果吗?
- 上图,我们看到只有在
setRandom
的时候ChildComponent render
会执行,现在,我们完成了组件不必要重的重复渲染的优化
useMemo 避免组件在每次渲染时都进行高开销的计算
优化前
import React, { Fragment, useState } from "react";
import { Button, Divider } from "antd";
const Admin = () => {
const [, setRandom] = useState<number>(0); //{1}
function getState() { //{2}
console.log("getState run"); //{3}
let temp = 0;
for (let index = 0; index < 1000; index++) {
temp += index;
}
return temp;
}
const computeValue = getState(); //{4}
console.log("computeValue", computeValue)//{5}
return (
<Fragment>
Admin
<Divider orientation="left">random</Divider>
<Button type="ghost" onClick={() => setRandom(Math.floor(Math.random() * 10 + 1))}>setRandom</Button> {/* {6} */}
</Fragment>
)
}
- 以上,我们定义了组件
Admin
, 在{1}
定义了setRandom
,并且在{6}
执行了setRandom
,我们需要让组件重新render
, 重点在方法{2}
, 我们在方法{2}
中比喻了大数据量的计算,并且在{4}
调用了方法{2}
, 需要验证的是{3}
是否会被重复打印 - 结果如下图,每次
setRandom {3}
都会被打印,这不合理,这个时候我们并不希望方法{2}
被执行,这里造成了大数据量的重复计算
useMemo
定义
- 官方文档
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo
的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴,而不是 useMemo
。
如果没有提供依赖项数组,useMemo
在每次渲染时都会计算新的值。
- 总结一下,
useMemo
帮我们缓存了某个值,比如组件中某个数组/对象需要通过大量计算得到,而这个值依赖于某一个state
,我们希望只在依赖的state
改变之后计算而不是任意state
改变之后都会计算,这无疑会造成性能上的问题。
优化后
const Admin = () => {
const [, setRandom] = useState<number>(0); //{1}
const memoizedValue = useMemo(() => getState(), []) // {7}
function getState() { //{2}
console.log("getState run"); //{3}
let temp = 0;
for (let index = 0; index < 1000; index++) {
temp += index;
}
return temp;
}
const computeValue = memoizedValue; //{4}
console.log("computeValue", computeValue)//{5}
return (
<Fragment>
Admin
<Divider orientation="left">random</Divider>
<Button type="ghost" onClick={() => setRandom(Math.floor(Math.random() * 10 + 1))}>setRandom</Button> {/* {6} */}
</Fragment>
)
}
- 以上我们新增了
{7}
缓存了函数{2}
的返回值,并且在{4}
更新了用法,优化结果如下图
- 可以看到,方法
{2}
中的大数据量的计算只会在第一次挂载的时候执行,后续使用了useMemo
缓存的值,我们完成了避免组件在每次渲染时都进行高开销的计算的优化(当然用useEffect
也是可以做到的,看具体场景)
源码浅析
[注] useCallback
, useMemo
源码都在react
项目中 /packages/react-reconciler/src/ReactFiberHooks.new.js
文件中,以下不在赘述。
useCallback
// mount阶段就是获取传入的回调函数和依赖数组,保存到hook的memorizedState中,然后返回回调函数
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
// update 阶段
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
// 从hook的memorizedState中获取上次保存的值[callback,deps]
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 比较新的deps和之前的deps是否相等
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 如果相等,返回memorized的callback
return prevState[0];
}
}
}
// 如果deps发生变化,更新hooks的memorizedState,并返回最新的callback
hook.memoizedState = [callback, nextDeps];
return callback;
}
useMemo
// mount阶段, 执行创建函数获得返回值
// 保存到hook的memorizedState中[nextValue, nextDeps]
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// update 阶段
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
// 获取新的deps
const nextDeps = deps === undefined ? null : deps;
// 从memorizedState中获取上次保存的值
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
// 比较新的deps和就的deps是否相等,如果两个值相等,返回旧的创建函数的返回值
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 如果dependents发生改变,hook中保存新的返回值和deps,并返回新的创建函数的返回值
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}