解析React Hooks中的典型闭包问题

11/20/2021 React

什么是闭包,可以看看我这篇文章

# 经典闭包问题

下面这段代码会控制台每过三秒就会输出 0,就算已经执行setCount(count + 1)

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

  function handleAlertClick() {
    setTimeout(() => {
      console.log('You clicked on: ' + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <button onClick={handleAlertClick}>print count</button>
    </div>
  );
}

下面这段代码每过两秒就输出 0

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

  useEffect(function () {
    setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
  }, []);

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>加1</button>
    </div>
  );
}

# 对于闭包问题解决的方法有几种:

前两种方式解决闭包引用问题,后两种解决旧 state 更新失败问题。

  • 记得在 useEffect, useMemo, useCallback 的第二个参数中添加依赖项。如果 useEffect 有副作用的话,记得在 return 函数中清除副作用。官方也在 eslint 中添加对应的提示,如果在上面的 hooks 中没有添加对应的依赖,就会有提示。

    function WatchCount() {
      const [count, setCount] = useState(0);
    
      useEffect(
        function () {
          const id = setInterval(function log() {
            console.log(`Count is: ${count}`);
          }, 2000);
          return () => {
            clearInterval(id); // 记得清除副作用
          };
        },
        [count]
      ); // 重点
    
      return (
        <div>
          {count}
          <button onClick={() => setCount(count + 1)}>加1</button>
        </div>
      );
    }
    
  • 使用 Ref,每次 useRef 只会创建一次,ref.current 保存的值会实时更新

    function WatchCount() {
      const [count, setCount] = useState(0);
      const ref = useRef(0);
      ref.current = count; // 重点
      useEffect(function () {
        const id = setInterval(function log() {
          console.log(`Count is: ${ref.current}`);
        }, 2000);
      }, []); // 重点
    
      return (
        <div>
          {count}
          <button onClick={() => setCount(count + 1)}>加1</button>
        </div>
      );
    }
    
  • 以函数的形式更新 state。如果更新值的时候依赖于旧的 state 话,就要以函数的形式去更新 state,函数的传参就上次更新的 state,否则就会陷入闭包陷阱,每次更新的值都是一样的。

    setCount((curCount) => curCount + 1);
    
  • 使用 useReducer, 通过一个简易的 Redux 来更新 state,这种方式最为简洁,方便,比较推荐这种方式。当然要视情况而决定。

    function reducer(count, action) {
      switch (action.type) {
        case 'add':
          return count + action.gap;
        default:
          return count;
      }
    }
    function WatchCount() {
      const [count, dispatch] = useReducer(reducer, 0);
    
      useEffect(function () {
        setInterval(function log() {
          dispatch({ type: 'add', gap: 1 });
        }, 2000);
      }, []);
    
      return <div>{count}</div>;
    }
    

# 底层原因

现在看 hooks 所针对的 FunctionComponnet
无论开发者怎么折腾,一个对象都只能有一个 state 属性或者 memoizedState 属性,可是,谁知道可爱的开发者们会在 FunctionComponent 里写上多少个 useStateuseEffect 等等呢?
所以,react 用了链表这种数据结构来存储 FunctionComponent 里面的 hooks。

function App() {
  const [count, setCount] = useState(1);
  const [name, setName] = useState('howard');
  useEffect(() => {}, []);
  const text = useMemo(() => {
    return 'ddd';
  }, []);
}

closure

这个对象的memoizedState属性就是用来存储组件上一次更新后的 state,next毫无疑问是指向下一个 hook 对象。
在组件更新的过程中,hooks 函数执行的顺序是不变的,就可以根据这个链表拿到当前 hooks 对应的Hook对象,函数式组件就是这样拥有了 state 的能力。
当前,具体的实现肯定比这三言两语复杂很多。

所以,知道为什么不能将 hooks 写到 if else 语句中了吧。因为这样可能会导致顺序错乱,导致当前 hooks 拿到的不是自己对应的 Hook 对象。

# Dan 老哥关于闭包问题的分析

  • 函数式组件在每一次渲染都有它自己的…所有, 你可以想象成每次  render  的时候都形成了一次快照, 保存了所有下面的东西, 每一份快照都是不同且独立的。
    即:
    • 每一次渲染都有自己的 props 和 state
    • 每一次渲染都有自己的事件处理函数
    • 每一次渲染都有自己的  useEffect()
  • class 组件之所以有时候"不太对"的原因是, React 修改了 class 中的  this.state  使其指向永远最新状态