最近用hooks
有点上头。
很多时候我们在学习新东西之后总是会很兴奋地去做各种尝试。在React hooks正式面世之后,团队也在很多业务中开始尝试使用这种新语法。除却提及最为广泛的useState
和useEffect
,也开始尝试用useMemo
和useCallback
来重构一些组件。
我们先来看看这两个hooks的基本形式。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
两个hooks的形式差不多,它们都接受两个参数:一个函数和一个可选的数组。他们的功能是,根据传入的依赖,来记录一个值或者函数。即,根据第二个参数传入的依赖数组,来判断记录的值或函数是否需要更新。其中,useMemo
的第一个参数是一个返回它所记录值的函数;useCallback
的第一个参数则是它所记录的方法本身。在官方给出的文档中,也明确表示:
useCallback(fn, deps)
is equivalent touseMemo(() => fn, deps)
.
hooks的源码其实都不是很复杂的亚子。
/**
* @link https://github.com/facebook/react/blob/16.8.6/packages/react/src/ReactHooks.js
*/
export function useCallback(
callback: () => mixed,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, inputs);
}
export function useMemo(
create: () => mixed,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, inputs);
}
/**
* @link https://github.com/facebook/react/blob/16.8.6/packages/react-reconciler/src/ReactFiberHooks.js
*/
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
通过areHookInputsEqual
方法来对前后两次传入的deps
数组进行比较,如果没有变化则返回上一次保存在memoizedState
中的值或方法。
顺着useMemo
&useCallback
的设计思路,就可以着手优化代码了。主要步骤如下:
useMemo
和useCallback
进行封装。和理想差别很大的是,优化的效果其实并没有什么明显的提升,甚至还让代码变得有些难以理解。
事后的反思让我再次审视了这两个hooks的定义,才发现我搞错了一个很重要的概念。我的出发点是,减少组件中各种值和方法因无关属性更新产生的重复声明。
对于不依赖组件内部数据的,将它们提取到组件外部定义为常量,能够保证它们不再受到组件更新的影响:这一部分是没问题的。
问题在于useCallback
并没有阻止它记录的函数被重复声明,事实上我们很容易忽视这个事实。实际上每次组件更新时它还是会重新声明一个新的函数;只是当依赖没有变化时,新声明的函数不会取代旧的函数,即只是避免了函数引用的更新。
同样,useMemo
也没有减少变量的声明,甚至相比于之前,虽然减少了一些不必要的计算,但是却带来了新的函数声明操作。
虽然没有达到预期的效果,但是useMemo
和useCallback
并不是没什么卵用的。联想到上一节的介绍,它们能够保证在依赖不变的情况下,所记录的值和方法的引用不变。
引用不变,这个熟悉的名词,很容易让人联想到PureComponent
这套东西。实际上,我觉得这才是useMemo
和useCallback
发挥作用的催化剂。
假如你的子组件使用了PureComponent
或者React.memo
,那么你可以考虑使用useMemo
和useCallback
封装提供给他们的props
,这样就能够充分利用这些组件的浅比较能力。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。