ahooks 源码笔记 - useMemoizedFn
随着 React
版本的升级,越来越多的 hooks
被应用起来,包括 React
自己提供的 useState
, useEffect
, useRef
... 和各种各样的自定义 hooks
。
本文主要介绍,ahooks
这个库中 useMemoizedFn
的实现以及一些自己的思考。这里假设各位已经对 React hooks
和 ahooks
有了基本的了解和使用经验,如不清楚可以先查看 React官方文档,ahooks在线文档。
useMemoizedFn
解决了什么问题?
-
先来简单看一下它的用法
const [name, setName] = useState('xiaoMing') const [age, seAge] = useState(20) // useCallback const memoFn = useCallback(() => { console.log('name', name, 'age', age) }, [name, age]) // useMemoizedFn const memoFn = useMemoizedFn(() => { console.log('name', name, 'age', age) })
-
它的功能和
useCallback
类似,不过使用更简单,不需要提供dep
数组。 -
解决了由 hook 中的
dep
引起的闭包问题,同时保证了函数调用的准确性,实时性。
它是如何实现的?
-
我们首先来考虑一下下面这种情况:
const callbackFn = useCallback(() => { console.log(`Current count is ${count}`); }, [count]); <ExpensiveTree showCount={memoizedFn} />
在上面的代码中,
callbackFn
的dep
必须包含count
,保证它被调用时能输出正确的count
,而不是错误的闭包值。但是这样的话,每次count
发生变化是,callbackFn
本身的引用会变化,会触发依赖callbackFn
的ExpensiveTree
组件re-render
。在ExpensiveTree
角度来看,其实这是一次多余的render
。 -
实际上,如果我们找到一种方法解决上面所说的问题,就实现了
useMemoizedFn
这个hook
,我们来看看需要解决的问题有哪些- callbackFn 的地址不能随 render 改变
- 要同时保证 count 的实时更新
- 并且 callbackFn 的引用地址不能变
- 不需要添加 dep 依赖
接下来开始解决这些问题,如下
function useMemoizedFn(fn) { // 这里可以拿到每次最新的 fn,并把它更新到 ref 中,这可以保证此 ref 能够持有最新的 fn 引用 const latestFn = useRef(fn); latestFn.current = fn; // 我们通过这个只初始化一次的 useRef 来构建一个函数调用外壳,保证这个外壳函数的引用不会发生变化 // 并且通过在内部持有最新函数的引用,来保证调用准确性 const memoizedFn = useRef((...args) => { latestFn.current?.(...args); }); return memoizedFn.current; }
-
到这里,我们已经实现了
useMemoizedFn
的所有功能,简单来说,这个hook
做的事情就是实时的维护函数的最新引用,并在适当的时候通过一个包装函数来调用它。
为什么 useCallback
在使用时,不更新 dep
,函数体内拿到的就是旧数据呢?
这其实可以从 useCallback
源码中找到答案
function mountCallback(callback, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看到,useCallback
在初始化时,调用的是 mountCallback
,创建了一个新的 hook
,并把 原始 callback
和 dep
存储在当前 hook
的 memoizedState
上。
再来看一下 useCallback
更新时做了什么?
function updateCallback(callback, deps) {
var hook = updateWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
var prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
var prevDeps = prevState[1];
// 新旧 dep 无变化,使用旧函数
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 否则,返回最新值
hook.memoizedState = [callback, nextDeps];
return callback;
}
可以看到,这里获取当上次存储的 callback
和 dep
,用旧的 dep
和最新的 dep
做一个浅比较。如果 dep
不同就用最新的 callback
和 dep
替换 memoizedState
,并返回。否则,就返回旧的 callback
。
那么在观察 useCallback
的使用方式: useCallback(() => { ... }, [...])
我们在 mountCallback
或 updateCallback
拿到的 callback
就是这个包含了当时上下文环境的箭头函数。在 dep
发生变化时,我们其实是用一个新的箭头函数来替换旧的箭头函数,而新的箭头函数中又持有着最新的数据引用,这就导致了如果没有及时更新函数引用,就会在调用时拿到旧的箭头函数引用,而旧的箭头函数持有的是旧的数据引用,从而拿到错误的过期数据。
实际上,在组件每次 render
时,被 useCallback
包裹的函数都会重新创建,只不过 useCallback
内部决定了是否使用这个最新的函数。
写在结尾
其实,很多开源库诸如 ahooks
,在平时开发中使用的频率是非常高的,但我自己实际也只是草草的看过一次源码,后面都是在查各种 API
来调用。正好最近社区有小伙伴在组织一个 ahooks源码共读
的活动,我也借此机会把自己的一些理解总结出来,让自己理解更加深刻的同时希望也可以帮助到更多的小伙伴,文中有任何错误也请各位能够指出。