react中无处不在的闭包

76 阅读2分钟

原文链接:www.developerway.com/posts/fanta…

问题

从useCallback入手

背景:在form表单中,有一个不得不依赖的很重的外部组件,这个组件需要传递两个属性,title和onclick,onlick的作用的是打印表单中的值。

  1. 因为组件很重,所以我们使用了react.memo来缓存避免无效渲染。
  2. 因为react.memo中的组件需要保证所有函数props被缓存了,memo才生效,所以我们使用useCallback来包裹onclick。
  3. 我们知道在useCallback中的变量都需要被添加到依赖,否则useCallback中的东西将永远不会更新,所以在useCallback的依赖中我们添加了value。
const HeavyComponentMemo = React.memo(HeavyComponent);

const Form = () => {
  const [value, setValue] = useState();

  const onClick = useCallback(() => {
    // submit data here
    console.log(value);
  
    // adding value to the dependency
  }, [value]);


  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome to the form"
        onClick={onClick}
      />
    </>
  );
};

这段代码看起来很平常,但问题在于,用户一输入,value就变了,意味着onClick的缓存便失效了,这样的话useCallback看起来并没有发挥作用,因此,HeavyComponent外层的memo也就失效了。

从react.memo入手

我们知道,react.memo可以有第二个参数,用来手动定义props的比较方法。我们可以下面这样:忽略onClick的比较。

const HeavyComponentMemo = React.memo(
  HeavyComponent,
  (before, after) => {
    return before.title === after.title;
  },
);

完整代码如下:

const HeavyComponentMemo = React.memo(
  HeavyComponent,
  (before, after) => {
    return before.title === after.title;
  },
);

const Form = () => {
  const [value, setValue] = useState();

  const onClick = () => {
    // submit our form data here
    console.log(value);
  };

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome to the form"
        onClick={onClick}
      />
    </>
  );
};

现在HeavyComponent被真正的缓存了,onClick已经影响不到HeavyComponent了。但是你会发现,如论你在input怎么输入,点击onClick中打印出来的,用于都是undefined,这就引出了今天的主角:闭包

原因分析:闭包

在react中,我们一直在无意识的创造闭包,react中的任何一个函数其实都形成了闭包。

const Component = () => {
  const [state, setState] = useState();

  const onClick1 = () => {
    // 闭包!
    console.log(state); // 正常打印
  };

  const onClick2 = useCallback(() => {
    // 闭包!
    console.log(state); // 正常打印
  });

  const onClick3 = useCallback(() => {
    // 闭包!
    console.log(state);  // 这个闭包永远不会更新,永远只会打印undefined
    
    // 没加依赖
  }, []);

  useEffect(() => {
    // 闭包!
    console.log(state); // 正常打印
  });


  return <>
    <input
        type="text"
        value={state}
        onChange={(e) => setValue(e.target.value)}
    />
    <button onClick={onClick} />
  </>;
};

最后的解决办法

const Form = () => {
  const [value, setValue] = useState();
  const ref = useRef();

  useEffect(() => {
    ref.current = () => {
      // 可以获取到最新value
      console.log(value);
    };
    // 因为下面没有加依赖,所以每次都会更新ref
  });

  const onClick = useCallback(() => {
    // 可以获取到最新ref
    ref.current?.();
  }, []);

  return (
    <>
      <input
        type="text"
        value={value}
        onChange={(e) => setValue(e.target.value)}
      />
      <HeavyComponentMemo
        title="Welcome closures"
        onClick={onClick}
      />
    </>
  );
};