现在越来越多人开始使用 React Hooks + 函数组件的方式构筑页面。函数组件简洁且优雅,通过 Hooks 可以让函数组件拥有内部的状态和副作用(生命周期),弥补了函数组件的不足。
但同时函数组件的使用也带来了一些额外的问题:由于函数式组件内部的状态更新时,会重新执行一遍函数,那么就有可能造成以下两点性能问题:
好在 React 团队也意识到函数组件可能发生的性能问题,并提供了 React.memo
、useMemo
、useCallback
这些 API 帮助开发者去优化他们的 React 代码。在使用它们进行优化之前,我想我们需要明确我们使用它们的目的:
在讲述 React.memo
的作用之前,我们先来思考一个问题:什么情况下需要重新渲染组件?
一般来讲以下三种情况需要重新渲染组件:
state
发生变化时context
发生变化时props
发生变化时现在我们先只关注第 3 点:props
发生变化时重新渲染,这种情况是一种理想情况。因为如果一个父组件重新渲染,即使其子组件的 props
没有发生任何变化,这个子组件也会重新渲染,我们称这种渲染为非必要的重新渲染。这时 React.memo
就可以派上用场了。
首先 React.memo
是一个高阶组件。
高阶组件(Higher Order Component)类似一个工厂:将一个组件丢进去,然后返回一个被加工过的组件。
被 React.memo
包裹的组件在渲染前,会对新旧 props
进行浅比较:
props
浅比较相等,则不进行重新渲染(使用缓存的组件)。props
浅比较不相等,则进行重新渲染(重新渲染的组件)。上述的解释可能会比较抽象,我们来看一个具体的例子:
import React, { useState } from 'react';
const Child = () => {
console.log('Child 渲染了');
return <div>Child</div>;
};
const MemoChild = React.memo(() => {
console.log('MemoChild 渲染了');
return <div>MemoChild</div>;
});
function App() {
const [isUpdate, setIsUpdate] = useState(true);
const onClick = () => {
setIsUpdate(!isUpdate);
console.log('点击了按钮');
};
return (
<div className="App">
<Child />
<MemoChild />
<button onClick={onClick}>刷新 App </button>
</div>
);
}
export default App;
复制代码
上例中:Child
是一个普通的组件,MemoChild
是一个被 React.memo
包裹的组件。
当我点击 button
按钮时,调用 setIsUpdate
触发 App 组件重新渲染(re-render)。
控制台结果如下:
如上图:
首次渲染时,Child
和 MemoChild
都会被渲染,控制台打印 Child 渲染了
和 memoChild
渲染了。
而当我点击按钮触发重新渲染后,Child
依旧会重新渲染,而 MemoChild
则会进行新旧 props
的判断,由于 memoChild
没有 props
,即新旧 props
相等(都为空),则 memoChild
使用之前的渲染结果(缓存),避免了重新渲染。
由此可见,在没有任何优化的情况下,React 中某一组件重新渲染,会导致其全部的子组件重新渲染。即通过 React.memo
的包裹,在其父组件重新渲染时,可以避免这个组件的非必要重新渲染。
需要注意的是:上文中的【渲染】指的是 React 执行函数组件并生成或更新虚拟 DOM 树(Fiber 树)的过程。在渲染真实 DOM (Commit 阶段)前还有 DOM Diff 的过程,会比对虚拟 DOM 之间的差异,再去渲染变化的 DOM 。不然如果每次更改状态都会重新渲染真实 DOM,那么 React 的性能真就爆炸了(笑)。
更多react面试题解答参见 前端react面试题详细解答
const memolized = useMemo(fn,deps)
React 的 useMemo 把【计算函数 fn
】和【依赖项数组 deps
】作为参数,useMemo 会执行 fn
并返回一个【缓存值 memolized
】,它仅会在某个依赖项改变时才重新计算 memolized
。这种优化有助于避免在每次渲染时都进行高开销的计算。具体使用场景可以参考下例:
import React, { useMemo, useState } from 'react';
function App() {
const [list] = useState([1, 2, 3, 4]);
const [isUpdate, setIsUpdate] = useState(true);
const onClick = () => {
setIsUpdate(!isUpdate);
console.log('点击了按钮');
};
// 普通计算 list 的和
console.log('普通计算');
const sum = list.reduce((previous, current) => previous + current);
// 缓存计算 list 的和
const memoSum = useMemo(() => {
console.log('useMemo 计算');
return list.reduce((previous, current) => previous + current);
}, [list]);
return (
<div className="App">
<div> sum:{sum}</div>
<div> memoSum:{memoSum}</div>
<button onClick={onClick}>重新渲染 App</button>
</div>
);
}
export default App;
复制代码
上例中:sum
是一个根据 list
得到的普通计算值,memoSum
是一个通过 useMemo
得到的 momelized 值(缓存值),并且依赖项为 list
。
如上图控制台中 log 所示:
sum
和 memoSum
都会根据 list
的值进行计算;list
没有改变,但是 sum
的值进行了重新计算,而 memoSum
的值则没有重新计算,使用了上一次的计算结果(memolized)。list
的值发生改变,sum
和 memoSum
的值都进行重新计算。总结:在函数组件内部,一些基于 State 的衍生值和一些复杂的计算可以通过 useMemo
进行性能优化。
const memolizedCallback = useCallback(fn, deps);
React 的 useCallback 把【回调函数 fn
】和【依赖项数组 deps
】作为参数,并返回一个【缓存的回调函数 memolizedCallback
】(本质上是一个引用),它仅会在某个依赖项改变时才重新生成 memolizedCallback
。当你把 memolizedCallback
作为参数传递给子组件(被 React.memo 包裹过的)时,它可以避免非必要的子组件重新渲染。
useCallback
与 useMemo
都会缓存对应的值,并且只有在依赖变动的时候才会更新缓存,区别在于:
useMemo
会执行传入的回调函数,返回的是函数执行的结果useCallback
不会执行传入的回调函数,返回的是函数的引用有很多初学者(包括以前的我)会有这样一个误区:在函数组件内部声明的函数全部都用 useCallback
包裹一层,以为这样可以通过避免函数的重复生成优化性能,实则不然:
useCallback
会造成额外的性能损耗,因为增加了额外的 deps
变化判断。useCallback
包一层,不仅显得臃肿,而且还需要手写 deps
数组,额外增加心智负担。React.memo
包裹。场景 1:useCallback
主要是为了避免当组件重新渲染时,函数引用变动所导致其它 Hooks 的重新执行,更为甚者可能造成组件的无限渲染:
import React, { useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(1);
const add = () => {
setCount((count) => count + 1);
};
useEffect(() => {
add();
}, [add]);
return <div className="App">count: {count}</div>;
}
export default App;
复制代码
上例中,useEffect
会执行 add
函数从而触发组件的重新渲染,函数的重新渲染会重新生成 add
的引用,从而触发 useEffect
的重新执行,然后再执行 add
函数触发组件的重新渲染... ,从而导致无限循环:
useEffect
执行 -> add
执行 -> setCount
执行 -> App
重新渲染 -> add
重新生成 -> useEffect
执行 -> add
执行 -> ...
为了避免上述的情况,我们给 add
函数套一层 useCallback
避免函数引用的变动,就可以解决无限循环的问题:
import React, { useCallback, useEffect, useState } from 'react';
function App() {
const [count, setCount] = useState(1);
// 用 useCallback 包裹 add ,只会在组件第一次渲染生成函数引用,之后组件重新渲染时,add 会复用第一次生成的引用。
const add = useCallback(() => {
setCount((count) => count + 1);
}, []);
useEffect(() => {
add();
}, [add]);
return <div className="App">count: {count}</div>;
}
export default App;
复制代码
场景 2:useCallback
是为了避免由于回调函数引用变动,所导致的子组件非必要重新渲染。(这个子组件有两个前提:首先是接收回调函数作为 props
,其次是被 React.memo
所包裹。)
const Child = React.memo(({ onClick }) => {
console.log(`Button render`);
return (
<div>
<button onClick={onClick}>child button</button>
</div>
);
});
function App() {
const [countA, setCountA] = useState(0);
const [countB, setCountB] = useState(0);
// 情况1:未包裹 useCallback
const onClick = () => {
setCountA(countA + 1);
};
// 情况2:包裹 useCallback
const onClick = useCallback(() => {
setCountA(countA + 1);
}, []);
return (
<div>
<div>countA:{countA}</div>
<div>countB:{countB}</div>
<Child onClick={onClick1} />
<button onClick={() => setCountB(countB + 1)}>App button</button>
</div>
);
}
复制代码
上例中,Child
子组件由 React.memo
包裹,接收 onClick
函数作为 props
参数。
onClick
未包裹 useCallback
,当点击 app button
时,触发重新渲染,onClick
重新生成函数引用,导致 Child
子组件重新渲染。onClick
包裹 useCallback
,当点击 app button
时,触发重新渲染,onClick
不会生成新的引用,避免了 Child
子组件重新渲染。上文叙述中,我们通过 React.memo
、useMemo
、useCallback
这些 API 避免了在使用函数组件的过程中可能触发的性能问题,总结为一下三点:
React.memo
包裹组件,可以避免组件的非必要重新渲染。useMemo
,可以避免组件更新时所引发的重复计算。useCallback
,可以避免由于函数引用变动所导致的组件重复渲染。原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。