跳到主要内容

场景

2023年02月22日
柏拉文
越努力,越幸运

一、缓存组件


未使用 useMemo 缓存之前: Appnum 的变化导致 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 缓存之后: Appnum 的变化导致 App 组件重新渲染, Component1 组件使用 useMemo 缓存, 所以 Component1 组件不会重新渲染。这是一种父对子的渲染控制方案,来源于一种情况,父组件 render ,子组件有没有必要跟着父组件一起 render ,如果没有必要,则就需要阻断更新流。

通过缓存React.element组件,实现了控制组件不必要的渲染,其原理为: 每次执行 render 本质上 createElement 会产生一个新的 props,这个 props 将作为对应 fiberpendingProps,在此 fiber 更新调和阶段, React 会对比 fiber 上老 oldProps 和新的 newProp ( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element 对象缓存起来,上面 props 也就和 fiberoldProps 指向相同的内存空间,也就是相等,从而跳过了本次更新。

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;