精准渲染
一、缓存变量
1.1 useMemo
未使用 useMemo
缓存之前: App
中 num
的变化导致 App
组件重新渲染, App
函数重新执行, uniqueId
重新生成新的值。造成的现象就是, Component1
组件与 App
中的 num
状态毫无关系, 但是却因为 num
的变化而重新渲染了, 造成渲染浪费。
function getUniqueIdByNanoID(size = 10) {
let id = '';
let i = size;
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
while (i--) {
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}
function getUniqueId(size) {
return getUniqueIdByNanoID(size);
}
function Component1(props){
const { uniqueId } = props;
console.log("Component1 Render", uniqueId);
return <div> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
const uniqueId = getUniqueId();
console.log("App Render");
const handClick = ()=>{
setNum(num + 1);
}
return <div>
<h3>App 组件</h3>
<Component1 { uniqueId } />
</div>
}
使用 useMemo
缓存之后: App
中 num
的变化导致 App
组件重新渲染, App
函数重新执行。 而 uniqueId
通过 useMemo
包裹进行缓存, 不会生成新的值, 因此, App
中的 num
状态的变化不会导致 Component1
组件重新渲染。
function getUniqueIdByNanoID(size = 10) {
let id = '';
let i = size;
const urlAlphabet =
'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict';
while (i--) {
id += urlAlphabet[(Math.random() * 64) | 0];
}
return id;
}
function getUniqueId(size) {
return getUniqueIdByNanoID(size);
}
function Component1(props){
const { uniqueId } = props;
console.log("Component1 Render", uniqueId);
return <div> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
const uniqueId = useMemo(()=> getUniqueId(), []);
console.log("App Render");
const handClick = ()=>{
setNum(num + 1);
}
return <div>
<h3>App 组件</h3>
<Component1 { uniqueId } />
</div>
}
二、缓存函数
2.1 useCallback
未使用 useCallback
缓存之前: App
中 num
的变化导致 App
组件重新渲染, App
函数重新执行, handleClick
重新生成新的函数。造成的现象就是, Component1
组件与 App
中的 num
状态毫无关系, 但是却因为 num
的变化而重新渲染了, 造成渲染浪费。
function Component1(props){
const { handleClick } = props;
console.log("Component1 Render");
return <div onClick={ handleClick }> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = ()=>{
setNum(num + 1);
}
return <div>
<h3>App 组件</h3>
<Component1 { handleClick } />
</div>
}
使用 useMemo
缓存之后: App
中 num
的变化导致 App
组件重新渲染, App
函数重新执行。 而 handleClick
通过 useCallback
包裹进行缓存, 不会生成新的函数, 因此, App
中的 num
状态的变化不会导致 Component1
组件重新渲染。
function Component1(props){
const { handleClick } = props;
console.log("Component1 Render");
return <div onClick={ handleClick }> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = useCallback(()=>{
setNum(num + 1);
}, [])
return <div>
<h3>App 组件</h3>
<Component1 { handleClick } />
</div>
}
三、缓存组件
3.1 memo
未使用 memo
缓存之前: App
中 num
的变化导致 App
组件重新渲染。造成的现象就是, Component1
组件与 App
中的 num
状态毫无关系, 但是却因为 num
的变化而重新渲染了, 造成渲染浪费。
function Component1(props){
const { num } = props;
console.log("Component1 Render", num);
return <div> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = ()=>{
setNum(num + 1);
}
return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<Component1 { num: 0 } />
</div>
}
使用 memo
缓存之后: App
中 num
的变化导致 App
组件重新渲染, Component1
组件使用 memo
包裹, 内部通过 Object.is
对比前后 num
的值有无变化, 由于 num
始终为 0
, 所以 Component1
组件不会重新渲染。
function Component1(props){
const { num } = props;
console.log("Component1 Render", num);
return <div> Component 1 组件</div>
}
const MemoComponent1 = memo(Component1);
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = ()=>{
setNum(num + 1);
}
return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<MemoComponent1 { num: 0 } />
</div>
}
3.2 useMemo
未使用 useMemo
缓存之前: App
中 num
的变化导致 App
组件重新渲染。造成的现象就是, Component1
组件与 App
中的 num
状态毫无关系, 但是却因为 num
的变化而重新渲染了, 造成渲染浪费。
function Component1(props){
const { num } = props;
console.log("Component1 Render", num);
return <div> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = ()=>{
setNum(num + 1);
}
return <div>
<h3 onClick={ handleClick }>App 组件</h3>
<Component1 { num: 0 } />
</div>
}
使用 useMemo
缓存之后: App
中 num
的变化导致 App
组件重新渲染, Component1
组件使用 useMemo
缓存, 所以 Component1
组件不会重新渲染。
function Component1(props){
const { num } = props;
console.log("Component1 Render", num);
return <div> Component 1 组件</div>
}
function App(){
const [num,setNum] = useState(0);
console.log("App Render");
const handleClick = ()=>{
setNum(num + 1);
}
const MemoComponent1 = useMemo(()=> <Component1 { num: 0} />, []);
return <div>
<h3 onClick={ handleClick }>App 组件</h3>
{ MemoComponent1 }
</div>
}
四、分离组件
4.1 将变化部分与不变部分分离
未分离之前: View
组件未订阅父组件中的任何状态, 理论上不应该随着 App
状态的改变而渲染。但是现在跟随者 num
状态的改变也发生了不必要的渲染。
function View(){
return <div> 嘻嘻哈哈 </div>
}
function App(){
const [num,setNum] = useState(0);
return <div>
<div>
<button onClick={ ()=> setNum(num+1) }> + 1</button>
<p> num is: { num }</p>
</div>
<View />
</div>
}
分离之后
: Num
组件中 num
状态的改变只影响 Num
组件自身, 对 App
组件和 View
组件毫无影响。
function View(){
return <div> 嘻嘻哈哈 </div>
}
function Num(){
const [num,setNum] = useState(0);
return <div>
<button onClick={ ()=> setNum(num+1) }> + 1</button>
<p> num is: { num }</p>
</div>
}
function App(){
return <div>
<Num />
<View />
</div>
}
五、控制组件
5.1 PureComponent
跟 shouldComponentUpdate
原理基本一致,通过对 props
和 state
的浅比较结果来实现 shouldComponentUpdate
if (this._compositeType === CompositeTypes.PureClass) {
shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}
shallowEqual
对应方法大致如下:
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* is 方法来判断两个值是否是相等的值,为何这么写可以移步 MDN 的文档
* https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: mixed, y: mixed): boolean {
if (x === y) {
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
return x !== x && y !== y;
}
}
function shallowEqual(objA: mixed, objB: mixed): boolean {
// 首先对基本类型进行比较
if (is(objA, objB)) {
return true;
}
if (typeof objA !== 'object' || objA === null ||
typeof objB !== 'object' || objB === null) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
// 长度不相等直接返回false
if (keysA.length !== keysB.length) {
return false;
}
// key相等的情况下,再去循环比较
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
5.2 shouldComponentUpdate
通过shouldComponentUpdate
生命周期函数来比对 state
和 props
,确定是否要重新渲染。默认情况下返回 true
表示重新渲染,如果不希望组件重新渲染,返回 false
即可