为什么第一个代码片段可以解决过时的闭包,而第二个代码片段不能?

Why the first code snippet can solve the stale closure and the second one can't?

提问人:sam liu 提问时间:8/25/2023 更新时间:8/25/2023 访问量:73

问:

感觉第一个和第二个没有什么区别。但是第一个代码片段可以解决过时的闭包。那么,为什么第二个不能呢?我真的想不通。谁能从JavaScript闭包的原理来解释它?

// ==========================  First code snippet ========================

let _formVal
export default function App() {
  const [formVal, setFormVal] = useState('');

_formVal =  formVal

  const handleSubmit = useCallback(() => {
    console.log('_formVal:', _formVal);
  }, []);

  return (
    <>
      <input
        onChange={(e) => {
          setFormVal(e.target.value);
        }}
        value={formVal}
      />
      <MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
    </>
  );
}

// ==========================  Second code snippet ========================

export default function App() {
  const [formVal, setFormVal] = useState('');

const _formVal =  formVal
  const handleSubmit = useCallback(() => {
    console.log('_formVal:', _formVal);
  }, []);

  return (
    <>
      <input
        onChange={(e) => {
          setFormVal(e.target.value);
        }}
        value={formVal}
      />
      <MemoziedSuperHeavyComponnent onSubmit={handleSubmit} />
    </>
  );
}
JavaScript ReactJS 闭包

评论

0赞 Bergi 8/25/2023
不能解决陈旧的闭包问题。它只是使用一个全局变量。如果渲染组件的多个实例,这将严重失败。

答:

0赞 Nick Parsons 8/25/2023 #1

在第一个示例中,只创建了一个变量,该变量位于顶部模块范围级别,因此该变量是函数组件读取和更新的变量。_formVal

在第二个示例中,为组件的每次渲染创建多个变量,因为每次重新渲染都会再次调用函数,从而在组件中重新创建变量,包括新作用域上下文中的变量。由于返回的函数引用永远不会改变,并且始终是在初始渲染上创建的第一个函数(由于空依赖关系),该函数将始终访问在其周围作用域中创建的第一个变量,而不是来自在其他作用域中创建的未来渲染的后续变量。_formValApp_formValuseCallback()[]_formVal

纯 JavaScript 的小例子:

let memoizedHandleSubmit;

function foo(x) {
  let handleSubmit = function() {
    return x++;
  };
  
  memoizedHandleSubmit ??= handleSubmit; // if `memoizedHandleSubmit` doesn't have a value assigned to it yet, assign the value, otherwise leave it
  return memoizedHandleSubmit;
}

const bar = foo(1);
console.log(bar()); // 1
const bar2 = foo(10);
console.log(bar2()); // 2, not 10

这里我们多次调用函数,每次调用它,都会创建一个新变量,然后创建函数。该函数实质上将保存通过第一次调用在函数中创建的第一个函数,后续调用将重用之前创建的函数(这就是您正在做的事情)。创建第一个函数时,它保存的闭包属于它所定义的范围,这意味着该函数可以访问在其外部定义的变量,例如 .当再次调用该函数时,将创建一个具有其自身值的新作用域,并创建一个新函数,但该作用域将被丢弃且未使用,而是返回旧函数。旧函数仍然只知道它最初定义的范围,因为这是它“保存”的闭包,因此它只知道原始调用的值,因此日志打印而不是 .fooxhandleSubmitmemoizeHandleSubmithandleSubmitfoofoofoohandleSubmituseCallbackhandleSubmithandleSubmitxfooxhandleSubmithandleSubmithandleSubmitx210


请注意,有多种方法可以解决过时的状态问题,对于这种特殊情况,您很可能希望用作依赖项,而不是 ,这将创建对更改时的新引用,从而允许您访问其中更新的状态值。[formVal]useCallback()[]handleSubmitformVal

0赞 Bayu Sri Hernogo 8/25/2023 #2

在您提供的两个代码片段中,乍一看它们看起来非常相似,但它们确实有细微的区别,这与闭包在 JavaScript 中的工作方式有关。让我们来分析一下差异:

第一个代码片段

let _formVal;
export default function App() {
  const [formVal, setFormVal] = useState('');

  _formVal = formVal;

  const handleSubmit = useCallback(() => {
    console.log('_formVal:', _formVal);
  }, []);

  return (
    <>
      <input
        onChange={(e) => {
          setFormVal(e.target.value);
        }}
        value={formVal}
      />
      <MemoizedSuperHeavyComponent onSubmit={handleSubmit} />
    </>
  );
}

第二个代码片段

export default function App() {
  const [formVal, setFormVal] = useState('');

  const _formVal = formVal;

  const handleSubmit = useCallback(() => {
    console.log('_formVal:', _formVal);
  }, []);

  return (
    <>
      <input
        onChange={(e) => {
          setFormVal(e.target.value);
        }}
        value={formVal}
      />
      <MemoizedSuperHeavyComponent onSubmit={handleSubmit} />
    </>
  );
}

现在,让我们关注关键区别:

第一个代码片段(带 let _formVal): 在此代码片段中,在组件外部声明。它在模块顶部声明为 use,使其成为具有模块级作用域的变量。这意味着可以从模块内的任何位置(包括组件内部)访问和修改它。执行此操作时,您将在模块的作用域中将组件状态的值分配给。这将创建对同一变量的引用。_formValAppletApp_formVal = formValformVal_formVal

第二个代码片段(带常量_formVal): 在此代码段中,在组件内部使用 .这意味着它是一个具有块级作用域的局部变量,特定于组件的功能。它不能在组件外部访问。_formValAppconstApp

现在,让我们讨论一下这些差异在闭包方面的影响:

在这两种情况下,当函数创建时,它使用 ,它从其周围的作用域中捕获变量以形成闭包。在第一个代码片段中,是一个模块级变量,因此可以在 创建的闭包中访问它。在第二个代码片段中,是一个局部变量,因此也可以在 创建的闭包中访问它。handleSubmituseCallback_formValuseCallback_formValuseCallback

在解决“过时的闭包”方面,两个代码片段的行为应该相似,因为在这两种情况下都是通过引用捕获的。执行函数时,它将始终记录 的当前值。_formValhandleSubmit_formVal

因此,这两个代码段的工作方式相似并可以解决“过时的闭包”问题的原因是,在这两种情况下都被捕获为参考,从而确保在调用函数时使用正确且最新的值。变量作用域(模块级与块级)的差异不会影响此上下文中的行为。_formValhandleSubmit