关于react.js:放弃使用-useCallback-吧我们有更好的方式

49次阅读

共计 3916 个字符,预计需要花费 10 分钟才能阅读完成。

自从 React Hooks 面世以来,咱们对其探讨便层出不穷。明天咱们来谈谈 React.useCallback 这个 API。先说论断:简直所有场景,咱们有更好的形式代替 useCallback

咱们先看看 useCallback 的用法

const memoizedFn = React.useCallback(() => {doSomething(a, b);
}, [a, b]);

React 官网把这个 API 当作 React.memo 的性能优化伎俩而打造。看介绍:

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项扭转时才会更新。当你把回调函数传递给通过优化的并应用援用相等性去防止非必要渲染(例如 shouldComponentUpdate)的子组件时,它将十分有用。

那咱们就来从性能优化的角度看看 useCallback

示例:

const ChildComponent = React.memo(() => {
  // ...
  return <div>Child</div>;
});

function DemoComponent() {function handleClick() {// 业务逻辑}

  return <ChildComponent onClick={handleClick} />;
}

DemoComponent 组件本身或追随父组件触发 render 时,handleClick 函数会被 从新创立
每次 renderChildComponent 参数中会承受一个新的 onClick 参数,这会间接击穿 React.memo,导致性能优化生效,并联动一起 render

当然,官网文档指出,在组件外部中每次追随 render 而从新创立函数的开销简直能够忽略不计。若不将函数传给自组件,齐全没有任何问题,而且开销更小。

接下来咱们用 useCallback 包裹:

// ...

function DemoComponent() {const handleClick = React.useCallback(() => {// 业务逻辑}, []);

  return <ChildComponent onClick={handleClick} />;
}

这样 handleClick 就是 memoized 版本,依赖不变的话则永远返回第一次创立的函数。但每次 render 还是创立了一个新函数,只是没有应用罢了。
React.memoPureComponent 相似,它们都会对传入组件的新旧数据进行 浅比拟,如果雷同则不会触发渲染。

接下来咱们在 useCallback 加上依赖:

function DemoComponent() {const [count, setCount] = React.useState(0);

  const handleClick = React.useCallback(() => {
    // 业务逻辑
    doSomething(count);
  }, [count]);

  // 其余逻辑操作 setState

  return <ChildComponent onClick={handleClick} />;
}

咱们定义了 count 状态作为 useCallback 的依赖。若 count 变动后,render 则会产生新的函数。这便会击穿 React.memo,联动子组件 render

const handleClick = React.useCallback(() => {
  // 业务逻辑
  doSomething(count);
}, []);

如果去除依赖,这时外部逻辑获得的 count 的值永远为初始值即 0,也就是拿不到最新的值。如果将外部的逻辑作为 function 提取进去作为依赖,这又会导致 useCallback 生效。

咱们看看 useCallback 源码

ReactFiberHooks.new.js

// 装载阶段
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
  // 获取对应的 hook 节点
  const hook = mountWorkInProgressHook();
  // 依赖为 undefiend,则设置为 null
  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;
  // 获取上次暂存的 callback 和依赖
  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;
}

通过源码不难发现,useCallback 实现是通过暂存定义的函数,依据前后依赖比拟是否更新暂存的函数,最初返回这个函数,从而产生闭包达到记忆化的目标。
这就间接导致了我想应用 useCallback 获取最新 state 则必须要将这个 state 退出依赖,从而产生新的函数。

大家都晓得,一般 function 能够变量晋升,从而能够相互调用而不必在意编写程序。如果换成 useCallback 实现呢,在 eslint 禁用 var 的时代,先申明的 useCallback 是无奈间接调用后申明的函数,更别说递归调用了。

组件卸载逻辑:

const handleClick = React.useCallback(() => {
  // 业务逻辑
  doSomething(count);
}, [count]);

React.useEffect(() => {return () => {handleClick();
  };
}, []);

在组件卸载时,想调用获取最新值,是不是也拿不到最新的状态?其实这不能算 useCallback 的坑,React 设计如此。

好了,咱们列出了一些无论是不是 useCallback 的问题。

  1. 记忆成果差,依赖值变动则从新创立
  2. 想要记忆成果好,又是个闭包,无奈获取最新值
  3. 上下文调用程序的问题
  4. 组件卸载时获取最新 state 的问题

我都想防止这些问题能够吗?拿来吧你!

咱们先看看用法

function DemoComponent() {const [count, setCount] = React.useState(0);

  const {method1, method2, method3} = useMethods({method1() {doSomething(count);
    },
    method2() {
      // 间接调用 method1
      this.method1();
      // 其余逻辑
    },
    method3() {setCount(3);
      // 更多...
    },
  });

  React.useEffect(() => {return () => {method1();
    };
  }, []);

  return <ChildComponent onClick={method1} />;
}

用法是不是很简略?还不必写依赖,这不仅完满避开了上述所有的问题。而且还让咱们的 function 聚合便于浏览。废话不多说,上源码:

export default function useMethods<T extends Record<string, (...args: any[]) => any>>(methods: T) {const { current} = React.useRef({
    methods,
    func: undefined as T | undefined,
  });
  current.methods = methods;

  // 只初始化一次
  if (!current.func) {const func = Object.create(null);
    Object.keys(methods).forEach((key) => {
      // 包裹 function 转发调用最新的 methods
      func[key] = (...args: unknown[]) => current.methods[key].call(current.methods, ...args);
    });
    // 返回给应用方的变量
    current.func = func;
  }

  return current.func as T;
}

实现很简略,利用 useRef 暂存 object,在初始化时给每个值包裹一份 function,用于转发获取最新的 function。从而既拿到最新值,又能够保障援用值在申明周期内永远不扭转。
完满,就这样~

那么是不是 useCallback 没有应用场景了呢?答案是否定的,在某些场景下,咱们须要通过 useCallback 暂存某个状态的闭包的值,以供需要时调用。比方音讯弹出框,须要弹出过后暂存的状态信息,而不是最新的信息。

最初,举荐一下我写的状态治理 Heo,useMethods 曾经蕴含其中,谢谢大家点个 star。前面会分享写 heo 库的动机,欢送大家关注微信公众号 前端星辰,给个反对。

正文完
 0