别再说react闭包问题是react的错了

2,178 阅读3分钟

难道是React的问题?

react中经常会出现闭包问题,最经典的一个案例就是延迟弹出弹窗

9.gif

在这个例子中,我们会发现,当我们点击按钮延迟三秒调用alert期间,我们疯狂点击让count+1,但是最后还是显示了0。为了搞清为什么会出现这种情况,首先我们要明白两个东西

  1. 函数是个特殊的对象
  2. 函数的作用域静态作用域

为什么函数是个特殊的对象跟这个有关,第一点就是函数上有个内部的[[Scopes]]属性,这个属性,决定了当时函数所在的作用域,我们改造刚才那个弹出的click,把它改成这样:

const handleClick = () => {
    const fn = () => {
      alert(count)
    };
    console.dir(fn)
    setTimeout(fn, 3000)
};

查看输出中的[[Scopes]],可以看到函数闭包中引用了count: image.png

此时count0。也就是说,作用域没有发生变化。

之后我们就要明白第二个点函数的作用域是静态作用域。就是说,函数的作用域是创建的时候就决定了,而我们点击setCount的时候,实际上Test函数被重新刷新了,而此时fn还是第一次的函数对象,为了更直观的查看这个,我们再次改造弹出的click

  const handleClick = () => {
    const fn = () => {
      alert(count)
    };
    setTimeout(() => {
      console.dir(fn);
      fn();
    }, 3000)
  };

image.png

可以看到,setTimeoutfn在回调触发时,环境还是第一次的环境

那既然这样,怎么解决这个问题呢,其实也很简单,把count变成reference值就行了,也就是使用useRef

const Test = function () {
  // const [count, setCount] = useState(0);
  const [update, setUpdate] = useState(0); // 提交状态刷新Test
  const count = useRef(0); // 使用ref

  const handleClick = () => {
    const fn = () => {
      alert(count.current)
    };
    setTimeout(() => {
      console.dir(fn);
      fn();
    }, 3000)
  };


  const handleAddClick = () => {
    count.current += 1;
    setUpdate(!update)
  }


  return (<>
    <button onClick={handleAddClick}>{count.current}</button>
    <button onClick={handleClick}>弹出</button>
  </>
  );
};

10.gif

可以看到解决了这个问题,当然我们可以查看此时的fn:

image.png

可以看到因为使用useRef,所以此时是实时的。也就是说,react闭包问题根本原因是函数对象的作用域的问题

谨慎使用useCallback

因为闭包问题的存在,useCallback就显得需要谨慎使用了,因为一旦刷新不及时,那返回的函数是不及时的。再看一个例子,点击count+1,但是useCallback包装的函数使用[]依赖:

const Test = function () {
  const [count, setCount] = useState<number>(0);

  const handleClick = useCallback(() => {
    setCount(count + 1);
  }, []); // 空依赖

  return <div onClick={handleClick}>click me {count}</div>;
};

11.gif

可以看到,此时我无论怎么点击,count最多就+1,主要原因就是此时useCallback没有返回最新的函数对象,我们改造一下,显示的更直观点:

const Test = function () {
  const [count, setCount] = useState<number>(0);

  const handleClick = useCallback(function a() {
    console.dir(a); // 输出a函数
    setCount(count + 1);
  }, []); // 空依赖

  return <div onClick={handleClick}>click me {count}</div>;
};

image.png

可以看到,无论怎么点击,a函数作用域中的count都是0,所以导致每次都是0+1。当然平时可以使用ahooksuseMemoizedFn,附上源码