跳到主要内容

场景

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

一、缓存组件


优化前: 对于子组件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 将作为对应 fiberpendingProps,在此 fiber 更新调和阶段, React 会对比 fiber 上老 oldProps 和新的 newProp ( pendingProps )是否相等,如果相等函数组件就会放弃子组件的调和更新,从而子组件不会重新渲染;如果上述把 element 对象缓存起来,上面 props 也就和 fiberoldProps 指向相同的内存空间,也就是相等,从而跳过了本次更新。

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 缓存之前: Appnum 的变化导致 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 缓存之后: Appnum 的变化导致 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;