likes
comments
collection
share

React系列- 图解:React Hook闭包问题

作者站长头像
站长
· 阅读数 14

前言:最近参与了同事的一个分享,其中里面的一个闭包问题引起了我的兴趣,由于时间问题,分享上并没有详解这里面的“前世今生”,带着好奇的心区学习了的这里面的一点一滴(虽然我卷,但我还是菜!~) 好了,接下来说一下具体的问题是什么?

React Hooks 是什么?

关于react Hooks的介绍,可以看我的其他文章:juejin.cn/post/723905…

React Hooks 的存储的数据结构

  • 首先,了解下Hook的结构 hook的数据结构是一个链表的结构,react组件的hooks会按照顺序存储在下面的链表结构中,这也是为什React不允许在if等条件语句中使用hooks,会改变hooks的前后关系引起不必要的问题
  export type Hook = {
  memoizedState: any, 
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: any,
  next: Hook | null,
};
  • 举例说明 Demo组件中声明了三个hooks,分别是useState和另外两个useEffect
  const Demo = () => {
    
    const [a, setA] = useState(1)

    useEffect(() => {
        setInterval(() => {
            console.log("hook state:", a)
        }, 1000)
    }, [])

    useEffect(() => {
        window.test2= a;
        let timer = setInterval(() => {
            console.log("window.test state:", window.test)
        }, 1000)
        return () => clearInterval(timer)
    }, [])
    return <Card className={styles.card_content}>
        <Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button>
        <Button onClick={() => window.test += 1}>点击window +1 </Button>
        {/* <div className={styles.card_body}>我是内容1</div>
        <div className={styles.card_body}>我是内容2</div> */}
    </Card>
}

查看Demo的react数据结构

React系列- 图解:React Hook闭包问题

React Hooks的闭包陷阱

  • 什么是React Hook闭包陷阱? 我个人根据现象的理解就是React Hook中使用的了对之前的state对象的引用,导致了该引用变量不会随组件的重新渲染而更新。(都是些什么乱七八糟的表述,感觉都很抽象又官方)\

    下面我们用例子说明 还是上面Demo的代码,在组件挂载后,useEffect中开启了一个setInterval定时器并每隔一秒输出一次state a的值。a的初始值为1

       const Demo = () => {
    
          const [a, setA] = useState(1)
    
          useEffect(() => {
              setInterval(() => {
                  console.log("hook state:", a)
              }, 1000)
          }, [])
          console.log("Demo-State-A:", a)
          // ...其他代码
          return <Card className={styles.card_content}>
              <Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button>
             {/** 其他代码... */}
          </Card>
      }
    

浏览器打印看下效果:会持续输出

React系列- 图解:React Hook闭包问题 点击点击hook A +1按钮,观察输出:点击了两次,组件的state a状态变更为3,但是定时器的输出仍为1 ,这种现象就是react 闭包陷阱。 React系列- 图解:React Hook闭包问题

  • 如何产生的?
  • 讲了这么多,那么这个闭包陷阱是如何产生的呢?其中最关键的原因就是React的更新逻辑,React更新时,生成一个新的states对象,而不是在原来的对象上修改。这就导致在闭包场景下会存在两个state对象,一个是老的state(以下称oldState),一个是新的state对象(以下称newState),因为oldState被闭包引用,所以一直没有被释放。所以呢,闭包中输出的值一直是oldState的值,而不是react更新后新的newState的值。这就解释了为什么打印的都是最初的值。 讲了这么多,可能会比较迷糊,下面我们用图例解释说明:

    react 更新后,会创建一个新的state对象(newState),前一次的State(oldState)对象若无引用将会被垃圾回收,详细流程见下图

React系列- 图解:React Hook闭包问题

明白了上面的更新过程,我们就知道了为什么造成了闭包。以下图详解。

React系列- 图解:React Hook闭包问题

闭包陷阱的解决方案

知道了上面的闭包原因,解决办法就会简单多了

  • 添加必要依赖 通过hooks(useEffect、useCallback等)的第二个参数:deps,保证每次更新时替换hook缓存的函数,读取到的是最新的state.

    React系列- 图解:React Hook闭包问题

  • 使用useRef 使用useRef,是由于useRef创建的对象,不受组件的影响,里面的值变化后,其本身的内存地址是不变的,所以可以被读取到。 代码示例:

        const Demo = () => {
    
      const [a, setA] = useState(1)
      const ref = useRef()
      ref.current= a;
      
      useEffect(() => {
          setInterval(() => {
              console.log("hook state:", ref.current)
          }, 1000)
      }, [a])
      console.log("Demo-State-A:", a)
    
      return <Card className={styles.card_content}>
          <Button onClick={() => setA(v => v + 1)}>点击hook A +1 </Button>
      </Card>
    
    }
    

打印测试结果,发现符合预期 React系列- 图解:React Hook闭包问题

  • 使用组件之外的变量(跟useRef一个道理) 该方式就是通过使用比如windowcookie上层组件的对象或context等引用不变的对象,都可以解决该问题。其原理跟useRef是一样的。

结语

由于文笔的原因,可能不是那么生动有趣,但是本着认真的态度,希望这篇文章还是能给读者带来一些帮助。文中有不对之处,欢迎大家指正!~