useCallback & useMemo

useCallback 与 useMemo

function Button({ onClick, color, children }) {
  const textColor = slowlyCalculateTextColor(this.props.color);
  return (
    <button
      onClick={onClick}
      className={"Button-" + color + " Button-text-" + textColor}
    >
      {children}
    </button>
  );
}
export default React.memo(Button); // ✅ Uses shallow comparison

不过在实际场景下,很多的 Callback 还是会被重新生成,我们还是需要在子组件中进行精细地 shouldComponentUpdate 控制。

闭包冻结

在 useCallback 的使用中,有时候我们会碰到所谓的闭包冻结的现象,即如下所示:

let Test = () => {
  /** Search base infos */
  const [searchID, setSearchID] = useState(0);

  /** Search info action */
  const onSearchInfos = useCallback(() => {
    let fetchUrl = "/api/getSearchInfos";
    let fetchParams = { searchID };
    fetch(fetchUrl, {
      method: "POST",
      body: JSON.stringify(fetchParams),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
      });
  }, []);

  return (
    <>
      <button
        onClick={() => {
          setSearchID(searchID + 1);
        }}
      >
        button1
      </button>
      <button
        onClick={() => {
          onSearchInfos();
        }}
      >
        button2
      </button>
    </>
  );
};

点击 button1 按钮,searchID 的值加 1,点击 button2 发送一个请求。然而问题是,当我们点击了四次 button1,把 searchID 的值更改到了 4,然后点击 button2,会发现,发送出去的请求,searhID 的值是 0。

原因分析

我们可以使用如下代码来理解:

storage = { a: 1 };

(() => {
  let b = storage.a;

  storage.f = () => {
    console.log(b);
  };
})();

storage.a = 2;

storage.f();

参考 Hooks 的实现原理,我们可以模拟写出如下的代码:

// React has some component-local storage that it tracks behind the scenes.
// useState and useCallback both hook into this.
//
// Imagine there's a 'storage' variable for every instance of your
// component.
const storage = {};

function useState(init) {
  if (storage.data === undefined) {
    storage.data = init;
  }

  return [storage.data, (value) => (storage.data = value)];
}

function useCallback(fn) {
  // The real version would check dependencies here, but since our callback
  // should only update on the first render, this will suffice.
  if (storage.callback === undefined) {
    storage.callback = fn;
  }

  return storage.callback;
}

function MyComponent() {
  const [data, setData] = useState(0);
  const callback = useCallback(() => data);

  // Rather than outputting DOM, we'll just log.
  console.log("data:", data);
  console.log("callback:", callback());

  return {
    increase: () => setData(data + 1),
  };
}

let instance = MyComponent(); // Let's 'render' our component...

instance.increase(); // This would trigger a re-render, so we call our component again...
instance = MyComponent();

instance.increase(); // and again...
instance = MyComponent();

解决方案

添加依赖

const onSearchInfos = useCallback(() => {
  // ...
}, [searchID]);

使用 Ref

interface IRef {
  current: any;
}

let Test = () => {
  /** Search base infos */
  const [searchID, setSearchID] = useState(0);

  /** 解决闭包问题 */
  const fetchRef: IRef = useRef(); // hooks为我们提供的一个通用容器,里面有一个current属性
  fetchRef.current = {
    //  为current这个属性添加一个searchID,每当searchID状态变更的时候,Test都会进行重新渲染,从而current能拿到最新的值
    searchID,
  };

  /** Search info action */
  const onSearchInfos = useCallback(() => {
    let fetchUrl = "/api/getSearchInfos";
    let fetchParams = { ...fetchRef.current }; // 解构参数,这里拿到的是外层fetchRef的引用
    fetch(fetchUrl, {
      method: "POST",
      body: JSON.stringify(fetchParams),
    })
      .then((res) => res.json())
      .then((res) => {
        console.log(res);
      });
  }, []);

  return (
    <>
      <button
        onClick={() => {
          setSearchID(searchID + 1);
        }}
      >
        button1
      </button>
      <button
        onClick={() => {
          onSearchInfos();
        }}
      >
        button2
      </button>
    </>
  );
};

useMemo

useMemo 的用法类似 useEffect,常常用于缓存一些复杂计算的结果。useMemo 接收一个函数和依赖数组,当数组中依赖项变化的时候,这个函数就会执行,返回新的值。

const sum = useMemo(() => {
  // 一系列计算
}, [count]);

Links

下一页