场景
一、缓存组件
未使用 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
组件不会重新渲染。这是一种父对子的渲染控制方案,来源于一种情况,父组件 render
,子组件有没有必要跟着父组件一起 render
,如果没有必 要,则就需要阻断更新流。
通过缓存React.element
组件,实现了控制组件不必要的渲染,其原理为: 每次执行 render
本质上 createElement
会产生一个新的 props
,这个 props
将作为对应 fiber
的 pendingProps
,在此 fiber
更新调和阶段, React
会对比 fiber
上老 oldProps
和新的 newProp
( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element
对象缓存起来,上面 props
也就和 fiber
上 oldProps
指向相同的内存空间,也就是相等,从而跳过了本次更新。
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>
}
二、缓存函数
描述: 对于函数组件而言,每一的渲染都代表着一次函数的执行,函数内部的变量如果不做处理,都会重新声明,这样会导致传递给子组件的数据都是新数据,造成子组件不必要的渲染(如果函数不传递子组件,则不需要处理)
实现: 通过 useMemo
来缓存函数、属性,使得函数组件重新渲染时,不会重新声明
import React, { PureComponent, useState, useCallback } from "react";
import ReactDOM from "react-dom";
class Child extends PureComponent {
constructor(props) {
super(props);
}
render() {
const { callback } = this.props;
console.log(callback);
console.log("Child 组件渲染");
return (
<div>
<h3>Child 组件</h3>
</div>
);
}
}
function App() {
const [num, setNum] = useState(0);
const handleClick = () => {
console.log("哈哈");
};
const handleClickMemo = useMemo(() => handleClick, []);
return (
<div>
<h3>App 组件</h3>
{num}
<button onClick={() => setNum(num + 1)}>改变</button>
<Child callback={handleClickMemo} />
</div>
);
}
三、缓存防抖函数
背景: 当我们在函数组件中使用debounce
的时候, 场景如下
import React from 'react';
import { debounce } from 'lodash';
function App() {
const onClick = debounce(
(e) => {
console.log(e);
},
3000,
{leading: true, trailing: false},
);
return (
<div className="App">
{num}
<button onClick={(e) => onClick(e)}>点击</button>
</div>
);
}
export default App;
我们多次点击事件, 通过debounce
是可以轻松的做到防抖的。但是如果防抖函数中有更改状态的操作时:
import React, {useState} from 'react';
import {debounce} from 'lodash';
function App() {
const [num, setNum] = useState(0);
const onClick = debounce(
() => {
setNum(num + 1);
},
3000,
{leading: true, trailing: false},
);
return (
<div className="App">
{num}
<button onClick={(e) => onClick(e)}>点击</button>
</div>
);
}
export default App;
那么每一次状态的改变,都会重新调用函数组件,函数组件中的debounce
函数会重建,所以导致平常用的debounce
函数中的timer
会成初始值,进而导致防抖失败。那么需要使用useMemo
或者useCallback
包裹debounce
函数。所以下面使用useDebounceFn
来实现React
函数组件的防抖功能
解决
import React, {useState} from 'react';
import {useDebounceFn} from 'ahooks';
function App() {
const [num, setNum] = useState(0);
const onClick = useDebounceFn(
(e) => {
console.log(e);
setNum(num + 1);
},
{
wait: 3000,
leading: true,
trailing: false,
},
);
return (
<div className="App">
{num}
<button onClick={(e) => onClick.run(e)}>点击</button>
</div>
);
}
export default App;
其中useDebounceFn
的实现是通过useMemo
实现的
import {useMemo} from 'react';
import {debounce} from 'lodash';
import useLatest from './useLatest';
import useUnmount from './useUnmount';
import type {DebounceOptions} from '../useDebounce/debounceOptions';
type noop = (...args: any[]) => any;
function useDebounceFn<T extends noop>(fn: T, options: DebounceOptions) {
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(() => {
return debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
);
}, []);
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
export default useDebounceFn;