从闭包的角度理解useState中的set函数

103 阅读4分钟

从闭包的角度理解useState中的set函数

文章开始之前, 我先问大家会用一下哪种方式来更新state:

import React, { memo, useCallback, useState, useRef } from 'react';

const App = memo(() => {
  const [counter, setCounter] = useState(100)
  // console.log(useState(100))
  
  const increment1 = () => {
    setCounter(counter + 1)
  }



  const increment2 = () => {
    setCounter(prevCounter => prevCounter + 1);
  }
  
  return (
    <div>
      <h2>Counter: {counter}</h2>
      //方式一: 通过直接传入一个值
      <button onClick={e => increment1()}>+1 by pass in a value</button>
      //方式二: 通过传递一个函数
      <button onClick={e => increment2()}>+1 of pass in a fucntion</button>
    </div>
  );
});

export default App;

先不着急探讨这两者的区别, 先来复习一下useState的基本用法:

 const [counter, setCounter] = useState(100)
 console.log(useState(100))

输出结果如下:

Aspose.Words.848a7930-9387-4913-932e-8ecd98b3b33a.001.png

这里的useState()返回的是一个数组, 包含两个值:

  1. 当前的 state。在首次渲染时,它将与你传递的 initialState 相匹配。
  2. set函数,它可以让你将 state 更新为不同的值并触发重新渲染

所以我们通常会使用数组的解构语法来定义useState():

const [counter, setCounter] = useState(100)

接下来我们用上面的两种方式来修改counter的值:

  1. 直接传入计算结果
setCounter(counter + 1)

2. 使用函数形式传递

setCounter(prevCounter => prevCounter + 1)

仿佛这两种方式都对counter进行了正确的修改, 那么两者到底有什么区别? 我们通过以下案例来说明:

import React, { memo, useCallback, useState, useRef } from 'react';

const App = memo(() => {
  const [counter, setCounter] = useState(100)
  console.log(useState(100))
  
  const increment1 = useCallback(() => {
    setCounter(counter + 1)
  }, []);



  const increment2 = useCallback(() => {
    setCounter(prevCounter => prevCounter + 1);
  }, []);
  
  return (
    <div>
      <h2>Counter: {counter}</h2>
      <button onClick={e => increment1()}>+1 by pass in a value</button>
      <button onClick={e => increment2()}>+1 of pass in a fucntion</button>
    </div>
  );
});

export default App;

通过以上案例, 我们发现方式一通过setCounter(counter+1)的方式, 只能进行一次" +1 "操作; 方式二只要进行" +1 "操作就会更新counter的值; 这是为什么呢?

先说为什么都会进行更新操作呢?

正是闭包的存在, 使得counter能够进行更新:

  • 先说一下我对闭包的理解:
    • 闭包和作用域链密切相关,在JavaScript中,闭包形成的原因就是内部函数引用了外部函数的变量,导致外部函数的作用域链被包含在内部函数的scope属性中,当内部函数被执行时,闭包就产生了;
  • 所以在increment(increment1 和 increment2)函数中都可以通过闭包来访问外部counter的状态, 再加上useCallback()的依赖是一个空数组,increment仅在组件被挂载的时候创建一次, 所以第一次都可进行" +1 "操作;
  • 闭包在这里发挥了重要作用,确保了状态更新的一致性和组件的正确渲染

但是increment1只能进行一次, increment2可以进行多次, 这是为什么呢?

  • setCounter函数是可以接收一个函数的, 其中的参数就是counter, 也就是说, set函数将state更新为不同的值之后会触发组件重新渲染, 即使useCallback中依赖的是一个空数组
  • 当你使用setCounter(prevCounter => prevCounter + 1)时,prevCounter实际上是一个由React传入的最新状态值,而不是你手动传递的。这种方式确保了状态更新的安全性和一致性,避免了可能出现的状态不一致问题

当我们把useCallback()换成组件重新渲染的情况:

1. 直接传入计算结果
setCounter(counter + 1);
  • 在这种情况下,你直接使用组件当前的counter值来计算新的状态;
  • 这意味着如果在这个调用之间(在你调用setCounter(counter + 1)和实际执行这个状态更新之间,组件可能因为其他操作而重新渲染),counter的值发生了变化,你可能会得到一个过时的状态值。这是因为counter在计算时可能不是最新的;
2. 使用函数形式传递
setCounter(prevCounter => prevCounter + 1);
  • 这里,你传递的是一个函数,这个函数的参数prevCounter是React当前的状态值;
  • React会在调用这个函数时,确保prevCounter是最新的状态。即使在调用期间发生了其他状态更新,你也能确保每次都是基于当前的状态进行更新, 这是React内部决定的;

总结

  • 直接传入值:容易导致引用旧状态,尤其在组件更新频繁时,可能会导致状态不一致。
  • 传入函数:保证每次状态更新都基于最新值,避免了潜在的错误,确保了状态的一致性。

结语: 在上面的代码引用useCallback()并不是要对代码进行优化, 而是想要通过useCallback的依赖为空, 在后面组件increment1更新的时候不重新渲染, 从而突出set函数通过传递函数的形式调用的高效性.