场景
一、缓存组件
优化前: 对于子组件Child
,Child
只接受了num1
,正常来讲只要num1
更新使Child
组件重新渲染就好,num2
更新带来的渲染对于Child
组件是不需要的。但是现在无论是num1
还是num2
更新,都会是Child
组件重新渲染。那么怎么样用缓存 element
来避免 children
没有必要的更新呢?
import React, { useState } from "react";
import ReactDOM from "react-dom";
function Child(props) {
const { num } = props;
console.log("Child 组件渲染");
return (
<div>
<h3>Child 组件</h3>
</div>
);
}
function App() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const handleChangeNum1 = () => {
setNum1(num1 + 1);
};
const handleChangeNum2 = () => {
setNum2(num2 + 1);
};
return (
<div>
<h3>App 组件</h3>
<Child num={num1} />
<button onClick={handleChangeNum1}>修改 num1</button>
<button onClick={handleChangeNum2}>修改 num2</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
优化后: 这是一种父对子的渲染控制方案,来源于一种情况,父组件 render
,子组件有没有必要跟着父组件一起 render
,如果没有必要, 则就需要阻断更新流。用 React.callback
, 将 React.element
组件缓存起来。将需要更新的值 num1
放在 deps
中, num1
改变,重新形成 element
对象,否则通过 useCallback
获取到上次的缓存值.
通过缓存React.element
组件,实现了控制组件不必要的渲染,其原理为: 每次执行 render
本质上 createElement
会产生一个新的 props
,这个 props
将作为对应 fiber
的 pendingProps
,在此 fiber
更新调和阶段, React
会对比 fiber
上老 oldProps
和新的 newProp
( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element
对象缓存起来,上面 props
也就和 fiber
上 oldProps
指向相同的内存空间,也就是相等,从而跳过了本次更新。
import React, { useState, useMemo, useCallback } from "react";
import ReactDOM from "react-dom";
function Child(props) {
const { num } = props;
console.log("Child 组件渲染");
return (
<div>
<h3>Child 组件</h3>
</div>
);
}
function App() {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const handleChangeNum1 = () => {
setNum1(num1 + 1);
};
const handleChangeNum2 = () => {
setNum2(num2 + 1);
};
return (
<div>
<h3>App 组件</h3>
{useCallback(<Child />, [num1])}
<button onClick={handleChangeNum1}>修改 num1</button>
<button onClick={handleChangeNum2}>修改 num2</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
二、缓存函数
未使用 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>
}
三、缓存防抖函数
背景: 当我们在函数组件中使用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
的可以通过useCallback
实现的
import {useCallback} 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 = useCallback(
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;