什么是闭包陷阱以及如何解决(面试)

163 阅读4分钟

什么是闭包陷阱以及如何解决(面试)

在准备面试的时候,我在总结完setState调用的过程是同步的还是异步的?这道题的时候,突然想到了之前总结的React中的什么是闭包陷阱?这道题;

我先总结一下我对setState调用的过程是同步的还是异步的?这道题的理解:

    setState的调用始终时异步的,无论是通过prevState => ({ count: prevState.count + 1 })还是传入普通的对象({ count: 1 }),setState的调用过程都是异步的

    为了避免不必要的更新和渲染,React会将setState中要更新的数据放入到等待队列中,等待所有的事件和生命周期完成后,批量更新这些状态,以提高性能。

    至于有的文章中会传入箭头函数的形式或者传递第二个参数为回调函数的形式会使setState变成同步更新,这种结论是错误的;这两种形式仍会将更新的数据传入更新队列;只不过是以箭头函数的形式,React会基于最新的state更新数据,形成了类似于同步更新的感觉;对于传入回调函数的形式,是在页面更新完成后进行回调,所以state中的数据也是最新的状态,但不是异步更新。

好的,明白了setState调用过程时同步的还是异步的?之后,我们可以得出结论:通过传入箭头函数的形式,React会基于最新的state更新数据,

那么引入到Hooks中的useState中也同样没毛病。

请看下面的代码:

--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown

import React, { memo, useState } from 'react'

const App = memo(() => {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(0)
  
  //通过传入普通的形式更新
  function btnClick1() {
    setTimeout(() => {
      setCount1(count1 + 1)
    }, 2000)
  }
  //通过传入箭头函数的形式更新
  function btnClick2() {
    setTimeout(() => {
      setCount2(count2 => count2 + 1)
    }, 2000)
  }

  return (
    <div>
      <h2>{ count1 }</h2>
      <button onClick={btnClick1}>闭包的形式+1</button>
      <h2>{ count2 }</h2>
      <button onClick={btnClick2}>箭头函数的形式+1</button>
    </div>
  )
})

export default App

上面的代码通过两个按钮的点击分别对count1和count2进行+1操作,count1通过普通的形式更新,count2通过传入箭头函数的形式更新;

当我们分别点击两个按钮后,都会对count1和count2在两秒后进行更新,没看出来其他毛病。我之前就是在这里没有理解清楚闭包陷阱的概念,我虽然通过闭包的概念去理解这个问题,但是始终没看出来闭包陷阱的问题在哪里,甚至怀疑我对闭包的理解有毛病?。直到我刚才在两秒内分别对btn1和btn2点击了五次,我才发现了我之前理解的闭包的概念是没有问题的,问题就出现了多次渲染上面。

那么问题就迎刃而解了,在两秒内,对闭包的形式+1箭头函数的形式+1点击五次,我们会发现以下的结果:

image.png

count1: 1

count2: 5

那么,来解释一下形成这两个结果的原因:

    count1为1,虽然点击了五次,但是count1因为闭包的存在,只要在两秒内,不管你点击多少次,它始终会引用外部变量count1,这时的count1始终为0,只有当setTimeout执行完成之后,count1 才变为1。

    count2为5,同样点击了五次,由于箭头函数的存在,React会基于最新的状态对count2进行更新,虽然是在点击两秒后进行更新,但是count2不再依赖于闭包,而是count2 的最新状态。点击一次count2+1,点击一次count2+1,直到点击五次后,count2会被渲染成5。

好的,还有一种方法去打破闭包(打破闭包的关键在于了解setState的更新是异步的 和 获取最新的state的状态)就是使用useRef,绑定count的值,下面是代码示例:

import React, { memo, useEffect, useRef, useState } from 'react'

const App = memo(() => {
  const [count1, setCount1] = useState(0)
  const [count2, setCount2] = useState(0)
  const count2Ref = useRef(count2)

  //关键点:在每次渲染完成后更新ref的值
  useEffect(() => {
    count2Ref.current = count2
  })
  
  function btnClick1() {
    setTimeout(() => {
      setCount1(count1 + 1)
    }, 2000)
  }
  function btnClick2() {
    setTimeout(() => {
      // setCount2(count2 => count2 + 1)
      setCount2(count2Ref.current + 1)
    }, 2000)
  }

  return (
    <div>
      <h2>{ count1 }</h2>
      <button onClick={btnClick1}>闭包的形式+1</button>
      <h2>{ count2 }</h2>
      <button onClick={btnClick2}>箭头函数的形式+1</button>
    </div>
  )
})

使用上面useRef的方法同样能够打破闭包的限制,获取到最新的count2的值,关键点在于使用useEffect获取到渲染完成后的ref的值


以上就是个人对于闭包陷阱的理解,纯属个人见解,若有解释有不妥的地方,还请各位大佬指正,共同进步,谢谢!