前言
当初越来越多人开始应用 React Hooks + 函数组件的形式构筑页面。函数组件简洁且优雅,通过 Hooks 能够让函数组件领有外部的状态和副作用(生命周期),补救了函数组件的有余。
但同时函数组件的应用也带来了一些额定的问题:因为函数式组件外部的状态更新时,会从新执行一遍函数,那么就有可能造成以下两点性能问题:
- 造成子组件的非必要从新渲染
- 造成组件外部某些代码(计算)的反复执行
好在 React 团队也意识到函数组件可能产生的性能问题,并提供了 React.memo
、useMemo
、useCallback
这些 API 帮忙开发者去优化他们的 React 代码。在应用它们进行优化之前,我想咱们须要明确咱们应用它们的目标:
- 缩小组件的 非必要从新渲染
- 缩小组件 外部的反复计算
1 应用 React.memo 防止组件的反复渲染
在讲述 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 面试题具体解答
2 应用 useMemo 防止反复计算
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
的值进行计算; - 当点击【从新渲染 App】按钮后,尽管
list
没有扭转,然而sum
的值进行了从新计算,而memoSum
的值则没有从新计算,应用了上一次的计算结果(memolized)。 - 当点击【往 List 增加一个数字】按钮后,
list
的值产生扭转,sum
和memoSum
的值都进行从新计算。
总结:在函数组件外部,一些 基于 State 的衍生值和一些简单的计算 能够通过 useMemo
进行性能优化。
3 应用 useCallback 防止子组件的反复渲染
const memolizedCallback = useCallback(fn, deps);
React 的 useCallback 把【回调函数 fn
】和【依赖项数组 deps
】作为参数,并返回一个【缓存的回调函数 memolizedCallback
】(实质上是一个援用),它仅会在某个依赖项扭转时才从新生成 memolizedCallback
。当你把 memolizedCallback
作为参数传递给子组件(被 React.memo 包裹过的)时,它能够防止非必要的子组件从新渲染。
useCallback 与 useMemo 异同
useCallback
与 useMemo
都会缓存对应的值,并且只有在依赖变动的时候才会更新缓存,区别在于:
useMemo
会执行传入的回调函数,返回的是 函数执行的后果useCallback
不会执行传入的回调函数,返回的是 函数的援用
useCallback 应用误区
有很多初学者(包含以前的我)会有这样一个误区:在函数组件外部申明的函数全副都用 useCallback
包裹一层,认为这样能够通过防止函数的反复生成优化性能,实则不然:
- 首先,在 JS 外部函数创立是十分快的,这点性能问题不是个问题(参考:React 官网文档:Hook 会因为在渲染时创立函数而变慢吗?)
- 其次,应用
useCallback
会造成额定的性能损耗,因为减少了额定的deps
变动判断。 - 每个函数用
useCallback
包一层,不仅显得臃肿,而且还须要手写deps
数组,额定减少心智累赘。
useCallback 正确的应用场景
- 函数组件外部定义的函数须要 作为其余 Hooks 的依赖。
- 函数组件外部定义的函数须要传递给其子组件,并且 子组件由
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
参数。
- 状况 1:
onClick
未包裹useCallback
,当点击app button
时,触发从新渲染,onClick
从新生成函数援用,导致Child
子组件从新渲染。 - 状况 2:
onClick
包裹useCallback
,当点击app button
时,触发从新渲染,onClick
不会生成新的援用,防止了Child
子组件从新渲染。
4 总结
上文叙述中,咱们通过 React.memo
、useMemo
、useCallback
这些 API 防止了在应用函数组件的过程中可能触发的性能问题,总结为一下三点:
- 通过
React.memo
包裹组件,能够防止组件的非必要从新渲染。 - 通过
useMemo
,能够防止组件更新时所引发的反复计算。 - 通过
useCallback
,能够防止因为函数援用变动所导致的组件反复渲染。