最新值与函数持久化之 useLatest 与 useMemoizedFn
“新”与“旧”
在JS
技术栈中,值是不是最新的其实困扰着很多的初学者。
很多时候我们想始终拿到最新的值,但是由于闭包的作用域问题可能拿到的是“旧”的值
比如还是拿我们源码解读 02 的例子来说,
import { useEffect, useState } from "react";
const useUnmount = (fn) => {
useEffect(
() => () => {
fn?.();
},
[]
);
};
const Child = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
useUnmount(() => {
console.log("count:", count);
});
return <div>{count}</div>;
};
export default function Test() {
const [flag, setFlag] = useState(false);
return (
<div>
<button
onClick={() => {
setFlag(!flag);
}}
>
点我
</button>
{!!flag ? <Child /> : null}
</div>
);
}
这里面要改是存在两处的问题的。
useUnmount
的实现中传入的函数不能取到最新的count
值interval
中每次累加的时候本身count
值始终是从 0 开始的,所以页面也始终是 1
如何修改,大家可以链接到原文去看下
因此我们这篇就来介绍下ahooks
源码实现和我们使用ahooks
时,关于useLatest
和useMemoizedFn
的一些知识点
“新” - useLatest
源码实现
import { useRef } from "react";
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
解读
useLatest
的实现非常简单。它使用了useRef
来保存传入的value
。每次组件渲染时,ref.current
都会被更新为最新的value
。这样,无论什么时候访问ref.current
,都能获取到最新的value
。
应用场景:
- 避免闭包问题:在某些情况下,我们可能会在回调函数中使用到组件的某些状态值,如果不使用
useLatest
,这些状态值可能会因为闭包的特性而无法获取到最新的值。例如,在事件处理函数中使用setInterval
或setTimeout
时,使用useLatest
可以确保访问到最新的状态。
import React, { useState, useEffect } from "react";
import { useLatest } from "ahooks";
export default () => {
const [count, setCount] = useState(0);
const [count2, setCount2] = useState(0);
const latestCountRef = useLatest(count);
useEffect(() => {
const interval = setInterval(() => {
setCount(latestCountRef.current + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setCount2(count2 + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<>
<p>count(useLatest): {count}</p>
<p>count(defult): {count2}</p>
</>
);
};
有同学肯定要问了,为什么使用useRef
就可以了
On the next renders,
useRef
will return the same object.
来自React
官网的描述,翻译过来的意思就是在后续的渲染时,始终给你返回相同的对象,对象的值引用不变,我们在需要的时候去修改或者读取它的current
,就始终得到了最新值。
PS:React
中,useRef
中current
的修改不会导致React
组件的重新渲染
“旧” - useMemoizedFn
提醒大家注意一下 这里的“旧”其实是一个缓存
和持久化
的感念
源码实现
import { useMemo, useRef } from "react";
import { isFunction } from "../utils";
import isDev from "../utils/isDev";
type noop = (this: any, ...args: any[]) => any;
type PickFunction<T extends noop> = (
this: ThisParameterType<T>,
...args: Parameters<T>
) => ReturnType<T>;
function useMemoizedFn<T extends noop>(fn: T) {
if (isDev) {
if (!isFunction(fn)) {
console.error(
`useMemoizedFn expected parameter is a function, got ${typeof fn}`
);
}
}
const fnRef = useRef<T>(fn);
// why not write `fnRef.current = fn`?
// https://github.com/alibaba/hooks/issues/728
fnRef.current = useMemo<T>(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
export default useMemoizedFn;
解读
useMemoizedFn
同样是利用useRef
来保存传入的函数fn
。每次组件渲染时,fnRef.current
都会被更新为最新的fn
。通过useCallback
返回一个记忆化的函数memoizedFn
,这个函数在执行时总是调用最新的fnRef.current
。
应用场景:
- 保持函数引用的一致性:在传递函数给子组件时,保持函数引用的一致性可以避免子组件的额外渲染。
import React from 'react';
import useMemoizedFn from './useMemoizedFn';
function ParentComponent() {
const handleAction = useMemoizedFn(() => {
console.log('Action handled');
});
return <ChildComponent onAction={handleAction} />;
}
function ChildComponent({ onAction }) {
return <button onClick={onAction}>Click me</button>;
}
export default ParentComponent;
}
- 依赖稳定的回调函数:在某些情况下,我们需要将一个回调函数传递给第三方库,这些库可能依赖回调函数的引用一致性。使用
useMemoizedFn
可以确保回调函数的引用在整个组件生命周期内保持稳定。
这里我们看下官网demo2
import { useMemoizedFn } from "ahooks";
import { message } from "antd";
import React, { useCallback, useRef, useState } from "react";
export default () => {
const [count, setCount] = useState(0);
const callbackFn = useCallback(() => {
message.info(`Current count is ${count}`);
}, [count]);
const memoizedFn = useMemoizedFn(() => {
message.info(`Current count is ${count}`);
});
return (
<>
<p>count: {count}</p>
<button
type="button"
onClick={() => {
setCount((c) => c + 1);
}}
>
Add Count
</button>
<p>
You can click the button to see the number of sub-component renderings
</p>
<div style={{ marginTop: 32 }}>
<h3>Component with useCallback function:</h3>
{/* use callback function, ExpensiveTree component will re-render on state change */}
<ExpensiveTree showCount={callbackFn} />
</div>
<div style={{ marginTop: 32 }}>
<h3>Component with useMemoizedFn function:</h3>
{/* use memoized function, ExpensiveTree component will only render once */}
<ExpensiveTree showCount={memoizedFn} />
</div>
</>
);
};
// some expensive component with React.memo
const ExpensiveTree = React.memo<{ [key: string]: any }>(({ showCount }) => {
const renderCountRef = useRef(0);
renderCountRef.current += 1;
return (
<div>
<p>Render Count: {renderCountRef.current}</p>
<button type="button" onClick={showCount}>
showParentCount
</button>
</div>
);
});
当我们点击add count
的时候,示例中 memoizedFn
是不会变化的,callbackFn
在 count 变化时变化。
总结:在 React 开发中掌握不变性的艺术与引用的智慧
由于这两个 Hook 在 ahooks 内部实现中被反复复用,对业务开发来说也是非常高频的工具 hook,因此我们这期重点拿出来讲了下。
React 开发中,我们常常面对状态管理、渲染优化和闭包陷阱等复杂问题。而利用useRef
、useLatest
和useMemoizedFn
等 hooks,我们可以更好地管理这些问题,从而编写出高效、稳定和易维护的代码。
不变性与引用的智慧
- 不变性:
- 不可变数据结构:在 React 中,状态更新的最佳实践是使用不可变数据结构。这不仅符合 React 通过浅比较进行高效重渲染的机制,还能避免因为直接修改对象或数组而引发的潜在问题。
- 状态更新:每次状态更新时,创建新的对象或数组,而不是直接修改原有的。这确保了 React 能够检测到状态的变化,从而正确触发重新渲染。
- 引用的智慧:
- useRef:
useRef
提供了一种方法,可以在组件的整个生命周期内持久化数据,而不触发组件的重新渲染。它在处理非状态数据的持久化存储方面非常有效,如保持某个值在多次渲染之间的一致性。 - useLatest:在处理闭包问题时,
useLatest
确保了在回调函数中总是可以访问到最新的状态或属性值。这在处理定时器、事件监听器等异步操作时尤其重要。 - useMemoizedFn:通过
useMemoizedFn
,可以确保函数引用在整个组件生命周期内保持一致,避免因函数引用变化而导致的子组件不必要的重新渲染。
- useRef:
通过理解和利用这些特性,我们可以编写出更加高效和健壮的 React 组件,提升应用的性能和用户体验。