对 React Hook的闭包陷阱的理解,有哪些解决方案?

简介: 对 React Hook的闭包陷阱的理解,有哪些解决方案?

hooks中 “奇怪”(其实符合逻辑) 的 “闭包陷阱” 的场景,同时,在许多 react hooks 的文章里,也能看到 useRef 的身影,那么为什么使用 useRef 又能摆脱 这个 “闭包陷阱” ?


搞清楚这些问题,将能较大的提升对 react hooks 的理解。


react hooks 一出现便受到了许多开发人员的追捧,或许在使用react hooks 的时候遇到 “闭包陷阱” 是每个开发人员在开发的时候都遇到过的事情


(以下react示范demo,均为react 16.8.3 版本)


一定遭遇过以下的这个场景:

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
}

在这个定时器里面去打印 count 的值,会发现,不管在这个组件中的其他地方使用 setCount 将 count 设置为任何值,还是设置多少次,打印的都是1

为什么会发生这么个情况说清楚,并且浅谈一些hooks其他的特性

1、一个熟悉的闭包场景

for ( var i=0; i<5; i++ ) {
    setTimeout(()=>{
        console.log(i)
    }, 0)
}

就不说为什么最终,打印的都是5的原因了。直接贴出使用闭包打印 0...4的代码:

for ( var i=0; i<5; i++ ) {
   (function(i){
         setTimeout(()=>{
            console.log(i)
        }, 0)
   })(i)
}

这个原理其实就是使用闭包,定时器的回调函数去引用立即执行函数里定义的变量,形成闭包保存了立即执行函数执行时 i 的值,异步定时器的回调函数才如我们想要的打印了顺序的值


其实,useEffect 的哪个场景的原因,useEffect 闭包陷阱场景的出现,是 react 组件更新流程以及 useEffect 的实现的自然而然结果


2、 浅谈hooks原理,理解useEffect 的 “闭包陷阱” 出现原因

首先,可能都听过react的 Fiber 架构,其实可以认为一个 Fiber节点就对应的是一个组件。对于 classComponent 而言,有 state 是一件很正常的事情,Fiber对象上有一个 memoizedState 用于存放组件的 state。现在看 hooks 所针对的 FunctionComponnet。一个对象都只能有一个 state 属性或者 memoizedState 属性,可是,谁知道开发者们会在 FunctionComponent 里写上多少个 useState,useEffect 等等 ? 所以,react用了链表这种数据结构来存储 FunctionComponent 里面的 hooks。比如:

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

在组件第一次渲染的时候,为每个hooks都创建了一个对象

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
};

最终形成了一个链表


useState---->useState---->useEffect---->useMemo


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


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


useEffect 接收了两个参数,一个回调函数和一个数组。数组里面就是 useEffect 的依赖,当为 [] 的时候,回调函数只会在组件第一次渲染的时候执行一次。如果有依赖其他项,react 会判断其依赖是否改变,如果改变了就会执行回调函数。说回最初的场景:

function App(){
    const [count, setCount] = useState(1);
    useEffect(()=>{
        setInterval(()=>{
            console.log(count)
        }, 1000)
    }, [])
    function click(){ setCount(2) }
}

组件第一次渲染执行 App(),执行 useState 设置了初始状态为1,所以此时的 count 为1。然后执行了 useEffect,回调函数执行,设置了一个定时器每隔 1s 打印一次 count。


但如果 click 函数被触发了,调用 setCount(2) 肯定会触发react的更新,更新到当前组件的时候也是执行 App(),之前说的链表已经形成了哈,此时 useState 将 Hook 对象 上保存的状态置为2, 那么此时 count 也为2了。然后在执行 useEffect 由于依赖数组是一个空的数组,所以此时回调并不会被执行。


这次更新的过程中根本就没有涉及到这个定时器,这个定时器还在坚持的,默默的,每隔1s打印一次 count。 注意这里打印的 count ,是组件第一次渲染的时候 App() 时的 count, count的值为1,因为在定时器的回调函数里面被引用了,形成了闭包一直被保存。


3、难道真的要在依赖数组里写上的值,才能拿到新鲜的值?

function App() {
  return <Demo1 />
}
function Demo1(){
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(10)
  const text = useMemo(()=>{
    return `num1: ${num1} | num2:${num2}`
  }, [num2])
  function handClick(){
    setNum1(2)
    setNum2(20)
  }
  return (
    <div>
      {text}
      <div><button onClick={handClick}>click!</button></div>
    </div>
  )
}

text 是一个 useMemo ,它的依赖数组里面只有num2,没有num1,却同时使用了这两个state。当点击button 的时候,num1和num2的值都改变了。那么,只写明了依赖num2的 text 中能否拿到 num1 最新鲜的值呢?


如果装了 react 的 eslint 插件,这里也许会提示错误,因为在text中你使用了 num1 却没有在依赖数组中添加它。 但是执行这段代码会发现,是可以正常拿到num1最新鲜的值的。


为什么呢,再说一遍,这个依赖数组存在的意义,是react为了判定,在本次更新中,是否需要执行其中的回调函数,这里依赖了的num2,而num2改变了。回调函数自然会执行, 这时形成的闭包引用的就是最新的num1和num2,所以,自然能够拿到新鲜的值。问题的关键,在于回调函数执行的时机,闭包就像是一个照相机,把回调函数执行的那个时机的那些值保存了下来。


4、为什么使用useRef能够每次拿到新鲜的值?

var A = {name: 'chechengyi'}
var B = A
B.name = 'baobao'
console.log(A.name) // baobao

对,这就是这个场景成立的最根本原因


也就是说,在组件每一次渲染的过程中。 比如 ref = useRef() 所返回的都是同一个对象,每次组件更新所生成的ref指向的都是同一片内存空间, 那么当然能够每次都拿到最新鲜的值了。犬夜叉看过把?一口古井连接了现代世界与500年前的战国时代,这个同一个对象也将这些个被保存于不同闭包时机的变量了联系了起来。

/* 将这些相关的变量写在函数外 以模拟react hooks对应的对象 */
let isC = false
let isInit = true; // 模拟组件第一次加载
let ref = {
  current: null
}
function useEffect(cb){
// 这里用来模拟 useEffect 依赖为 [] 的时候只执行一次。
if (isC) return
isC = truet
cb()
}
function useRef(value){
// 组件是第一次加载的话设置值 否则直接返回对象
  if ( isInit ) {
    ref.current = value
    isInit = false
  }
  return ref
}
function App(){
  let ref_ = useRef(1)
  ref_.current++
  useEffect(()=>{
    setInterval(()=>{
      console.log(ref.current) // 3
    }, 2000)
  })
}
// 连续执行两次 第一次组件加载 第二次组件更新
App()
App()

所以,提出一个合理的设想。只要我们能保证每次组件更新的时候,useState 返回的是同一个对象的话?也能绕开闭包陷阱这个情景吗?

function App() {
  // return <Demo1 />
  return <Demo2 />
}
function Demo2(){
  const [obj, setObj] = useState({name: 'chechengyi'})
  useEffect(()=>{
    setInterval(()=>{
      console.log(obj)
    }, 2000)
  }, [])
  function handClick(){
    setObj((prevState)=> {
      var nowObj = Object.assign(prevState, {
        name: 'baobao',
        age: 24
      })
      console.log(nowObj == prevState)
      return nowObj
    })
  }
  return (
    <div>
      <div>
        <span>name: {obj.name} | age: {obj.age}</span>
        <div><button onClick={handClick}>click!</button></div>
      </div>
    </div>
  )
}

在执行 setObj 的时候,传入的是一个函数。然后 Object.assign 返回的就是传入的第一个对象。

就是在设置的时候返回了同一个对象。

执行这段代码发现,确实点击button后,定时器打印的值也变成了:

{
    name: 'baobao',
    age: 24 
}


相关文章
|
6月前
|
前端开发 Android开发 iOS开发
移动端自适应解决方案vw(以react为例)
移动端自适应解决方案vw(以react为例)
144 0
|
6月前
|
存储 前端开发 JavaScript
React闭包陷阱产生的原因是什么,如何解决
react闭包陷阱产生的原因是由于在React组件中使用了异步操作(如定时器、事件监听等)时,闭包会保留对旧状态的引用,导致更新后的状态无法正确地被获取或使用。
135 0
|
1月前
|
设计模式 缓存 前端开发
React中样式解决方案有哪些?
本文首发于微信公众号“前端徐徐”,探讨了React开发中的样式管理方法,包括内联样式、常规CSS、CSS-Module、CSS-in-JS及使用CSS框架等五种常见方案,分析了各自的优缺点,帮助开发者根据项目需求选择合适的样式解决方案。
35 0
|
3月前
|
前端开发 JavaScript API
|
3月前
|
开发者 安全 UED
JSF事件监听器:解锁动态界面的秘密武器,你真的知道如何驾驭它吗?
【8月更文挑战第31天】在构建动态用户界面时,事件监听器是实现组件间通信和响应用户操作的关键机制。JavaServer Faces (JSF) 提供了完整的事件模型,通过自定义事件监听器扩展组件行为。本文详细介绍如何在 JSF 应用中创建和使用事件监听器,提升应用的交互性和响应能力。
37 0
|
3月前
|
前端开发 JavaScript 中间件
【前端状态管理之道】React Context与Redux大对决:从原理到实践全面解析状态管理框架的选择与比较,帮你找到最适合的解决方案!
【8月更文挑战第31天】本文通过电子商务网站的具体案例,详细比较了React Context与Redux两种状态管理方案的优缺点。React Context作为轻量级API,适合小规模应用和少量状态共享,实现简单快捷。Redux则适用于大型复杂应用,具备严格的状态管理规则和丰富的社区支持,但配置较为繁琐。文章提供了两种方案的具体实现代码,并从适用场景、维护成本及社区支持三方面进行对比分析,帮助开发者根据项目需求选择最佳方案。
61 0
|
3月前
|
前端开发
React 中的 Hook 概念
【8月更文挑战第31天】
33 0
|
6月前
|
JavaScript
react+typescript通过window.xxx挂载属性报错的解决方案
react+typescript通过window.xxx挂载属性报错的解决方案
227 0
|
4月前
|
前端开发
React useImperativeHandle Hook
【7月更文挑战第1天】React useImperativeHandle Hook
28 3
|
4月前
|
前端开发 JavaScript 数据格式
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
react18【系列实用教程】Hooks (useState,useReducer,useRef,useEffect,useContext,useMemo,useCallback,自定义 Hook )
93 1