react hooks个人笔记- rerender会造成throttle的一些问题

1,276 阅读2分钟

rerender会造成throttle的一些问题

初步遇到问题

import React, { useState } from "react";
import { throttle } from "lodash";

function App() {
  const [count, setCount] = useState(0);

  const onChange = throttle(arg => {
    console.log("onChange", arg);
    setCount((preCount) => preCount + 1);
  }, 1000);

  return (
    <div>
      {count} <br />
      <button onClick={() => onChange(count)}>click</button>
    </div>
  );
}

直接使用的话,因为按钮的点击触发的 onChange 中,有 setCount 的存在,而 setCount 会导致组件的 rerender,结果就是 onChange 会被重复赋值--也就是说,每次点击按钮都会触发一个新的 onChange 函数。
结果就是 thtottle 看起来是无效的。

去掉 setCount 就不会有这个问题,但是项目中的业务逻辑显然不会不触发组件的 rerender。那自定义一个 useThrottle 钩子,来尝试解决这个问题。

这个钩子的设计看起来有两个方向:

  • 保证 useThrottle 函数的返回值在每次 rerender 后不变
  • 保证即使 useThrottle 函数的返回值会变化,但是内部的定时器不变

总的来说,需要一个能够在 rerender 中,持久保存的数据。

方向一:保证 useThrottle 函数的返回值在每次 rerender 后不变

其实这里涉及到 ahooks/useCreation 中所说的:

而相比于 useRef,你可以使用 useCreation 创建一些常量,这些常量和 useRef 创建出来的 ref 有很多使用场景上的相似,但对于复杂常量的创建,useRef 却容易出现潜在的性能隐患。

并且举出了一个例子:

const a = useRef(new Subject()) // 每次重渲染,都会执行实例化 Subject 的过程,即便这个实例立刻就被扔掉了

const b = useCreation(() => new Subject(), []) // 通过 factory 函数,可以避免性能隐患

这个钩子提出了一个创建常量的想法,即被创建的变量/函数在首次创建或者 deps 发生变化之前,不需要被再次创建,可以参考他们的源代码:

function useCreation(factory = () => {}, deps = []) {
  const { current } = useRef({ deps, obj: undefined, initialized: false });

  if (current.initialized === false || !depsAreSame(current.deps, deps)) {
    current.deps = deps;
    current.obj = factory();
    current.initialized = true;
  }
  return current.obj;
}

function depsAreSame(oldDeps = [], deps = []) {
  if (oldDeps === deps) return true;
  for (const i in oldDeps) {
    if (oldDeps[i] !== deps[i]) return false;
  }
  return true;
}

以这个为思路,可以实现一个简单的 useThrottle 钩子:

import React, { useState } from "react";
import { throttle } from "lodash";
import { useCreation } from 'ahooks';

function App() {
  const [count, setCount] = useState(0);

  const onChange = useThrottle(arg => {
    console.log("onChange", arg);
    setCount((preCount) => preCount + 1);
  }, 1000);

  return (
    <div>
      {count} <br />
      <button onClick={() => onChange(count)}>click</button>
    </div>
  );
}

// 自定义 useThrottle 钩子
function useThrottle(fn = () => {}, time = 300) {
  return useCreation(() => throttle(fn, time));
}

当然 ahooks 是有 useThrottleFn 钩子的,可以直接使用 ahooks/useThrottleFn.

方向二:保证即使 useThrottle 函数的返回值会变化,但是内部的定时器不变

用 useRef 来保存 timer:

function useThrottle(fn = () => {}, time = 300) {
  const { current } = useRef({ timer: null });

  return (...args) => {
    if (current.timer) return;
    fn.apply(null, args);
    current.timer = setTimeout(() => {
      clearTimeout(current.timer);
      current.timer = null;
    }, time);
  };
}

这样也能实现 throttle 的效果。