性能:React 实战优化技巧 之 函数闭包
子组件使用了 React.memo
,为什么 “prop 值未发生改变”,子组件依然被重新渲染了?
🚧 示例:点击子组件中按钮,获取 input 数据进行提交(常见于表单)
index.tsx
import Author from './Author.tsx';
export default function Index() {
const [val, setVal] = useState('');
const consoleValue = () => {
console.log(val);
}
return (
<>
<input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
<Author name="李刚" onClick={consoleValue} />
</>
);
}
Author.tsx
export default function Author({ name, onClick }: { name: string; onClick: () => void }) {
console.log('Author');
return (
<div>
<button onClick={onClick}>{name}</button>
</div>
);
}
🐋 现象:input 每次输入值,<Author>
组件就被重新渲染一次【prop onClick 发生了变化】!
🐒 使用 memo
,在 props 没有改变的情况下跳过重新渲染!
index.tsx
const AuthorMemo = React.memo(Author);
export default function Index() {
const consoleValue = useCallback(() => {
console.log(val);
}, [val]);
return (
<>
{/* 省略 */}
<AuthorMemo name="李刚" onClick={consoleValue} />
</>
);
}
这里需要使用 useCallback(fn, dependencies)
来处理渲染期间传递普通函数,避免传递给组件的 props 始终不同!
因为 <Author>
中要获取最新的 val
值,因此 useCallback()
中追加了 val
作为 dependencies
。
每次 <input>
输入,val
值都发生变化、从而导致 consoleValue
重新生成,因此 <AuthorMemo>
依然每次会重新渲染!
1️⃣ 传递依赖项数组: 初始渲染后以及依赖项变更后 运行
const consoleValue = useCallback(() => {
// ...
}, [val]); // val 变更时返回一个新的函数
2️⃣ 传递空依赖项数组:仅在 初始渲染后 运行
const consoleValue = useCallback(() => {
// ...
}, []); // 初始化后,不会再执行
3️⃣ 不传递依赖项数组:每次渲染之后 运行
const consoleValue = useCallback(() => {
// ...
}); // 每一次都返回一个新函数:没有依赖项数组
🐇 上述问题应该如何解呢?
【有缺陷】方案一:memo
支持自定义 arePropsEqual
来确定是否重新渲染!
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
const AuthorMemo = React.memo(Author, (_prevProps, nextProps) => {
/**
* 可选参数 arePropsEqual:一个函数,接受两个参数:组件的前一个 props 和新的 props。
* 如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。
* 通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop。
*/
return _prevProps.name === nextProps.name;
});
export default function Index () {
const consoleValue = useCallback(() => {
console.log(val);
}, [val]);
}
该方案的问题:获取的 val
值为一直为初始值,无法获取输入的最终 val
值。
🐇 问题分析:典型的 React 中的闭包问题。
React 中:组件内的每个函数都是一个闭包,因为组件本身只是一个函数。
理论上,当 val
发生变化时,consoleValue
函数会被重新创建,从而捕获最新的 val
值。然而,如果 AuthorMemo
没有重新渲染,或者 Author
组件内部没有正确处理 onClick
的更新【React.memo
比较算法导致 _prevProps.name === nextProps.name;
】,可能会导致 consoleValue
没有捕获到最新的 val
值。
【推荐】方案二:ref
+ useEffect
组合实现。
借助 ref 每次渲染间存储信息及修改不会触发渲染的特性;
const AuthorMemo = React.memo(Author);
export default function Index() {
const [val, setVal] = useState('');
const ref = useRef<(() => void) | undefined>();
// 省略 dependencies 参数,则在每次重新渲染组件之后,将重新运行 Effect 函数
useEffect(() => {
// 每次更新,指向一个新的函数
// .current 属性可以随时被更新,因此它不会受到闭包的限制
ref.current = () => {
console.log(val);
};
});
// 依赖数组为空 [] 在整个组件的生命周期中只会被创建一次(初始化)
const consoleValue = useCallback(() => {
ref.current?.();
}, []);
return (
<>
<input type="text" value={val} onChange={(e) => setVal(e.target.value)} />
<AuthorMemo name="李刚" onClick={consoleValue} />
</>
);
}
ref
用于渲染之间 存储信息(普通对象存储的值每次渲染都会重置);useEffect(() => {})
每次渲染执行;ref.current = ...
改变ref.current
属性时,React 不会重新渲染组件;
const consoleValue = useCallback(() => {}, [])
只初始渲染运行、确保了consoleValue
不发生变化(useCallback 在多次渲染中缓存函数)。
useState(initialState) useRef(initialValue)
initialState
:这个参数在首次渲染后被忽略。
🐇 原理分析:为什么没有闭包问题
- 为了让函数能够访问最新状态,每次重新渲染时都需要重新创建函数,这是无法避免的,这也是闭包的本质,与
React
无关; - 利用
Ref
是一个可变对象这一特性,从而摆脱 “过期闭包” 的问题。我们可以在过期闭包之外更改ref.current
,然后在闭包之内访问它,就可以获取最新的数据。
通过 useRef
和 useEffect
动态更新引用的函数,避免了闭包问题。consoleValue
函数虽然在整个组件生命周期中保持不变,但它通过调用 ref.current
来间接访问最新的 val
值。
1️⃣ 传递依赖项数组: 初始渲染后以及依赖项变更的重新渲染后 运行
useEffect(() => {
// ...
}, [a, b]); // 如果 a 或 b 不同则会再次运行
2️⃣ 传递空依赖项数组:仅在 初始渲染后 运行
useEffect(() => {
// ...
}, []); // 不会再次运行(开发环境下除外)
3️⃣ 不传递依赖项数组:每次渲染之后 运行
useEffect(() => {
// ...
}); // 总是再次运行